Reputation: 19335
The Objective-C method encoding produced by the Swift compiler for swift methods with blocks seems to use syntax not documented anywhere.
For instance the method:
func DebugLog2(message: String) async -> String
has the method encoding:
v32@0:8@"NSString"16@?<v@?@"NSString">24
and there are at least three characters ("
, <
, and >
) not covered in the documentation:
The NSMethodSignature
class also doesn't like this encoding:
[NSMethodSignature signatureWithObjCTypes:"\"NSString\"16@?<v@?@\"NSString\">24"];
results in:
'+[NSMethodSignature signatureWithObjCTypes:]: unsupported type encoding spec '"' in '"NSString"16@?<v@?@"NSString">24''
I tried looking through Swift's code, and it seems there's something called "extended method type encodings":
but I got lost trying to figure out where in the Swift codebase the method type encoding is generated.
Related questions that don't answer this:
Sample code:
decodeProto()
@objc
class TestClass : NSObject {
@objc
func DebugLog(message: String) -> String {
print(message)
return message
}
@objc
func DebugLog2(message: String) async -> String {
do {
try await Task.sleep(nanoseconds: 1_000_000_000)
} catch {}
print(message);
return message;
}
}
func decodeProto() {
var methodCount : UInt32 = 1
let methodList = class_copyMethodList(TestClass.self, UnsafeMutablePointer<UInt32>(&methodCount))!
for i in 0..<Int(methodCount) {
let method = methodList[i];
let desc = method_getDescription (method);
let name = desc.pointee.name!
let types = String(validatingUTF8: desc.pointee.types!)!
print("Selector: \(name) Description: \(types)")
}
}
prints:
Selector: DebugLog2WithMessage:completionHandler: Description: v32@0:8@"NSString"16@?<v@?@"NSString">24
Selector: DebugLogWithMessage: Description: @24@0:8@16
Selector: init Description: @16@0:8
But why do I need this?
I need to compute a maximum size for the stack frame, and in order to do that I parse the method encoding. The reason I need to compute the minimum size is because I intercept calls to objc_msgSend to add Objective-C exception handling before I call the actual objc_msgSend, so it's something like:
intercept_objc_msgSend (...) {
@try {
objc_msgSend (...);
} @catch (NSException *ex) {
handleException (ex);
}
}
Note that it's not possible to create a generic interception method in C, so I've done it in assembly code. The generic interception code needs to allocate space for and copy the original arguments (stack frame), and in order to do that I parse the method encoding to figure out how big each argument is.
The count doesn't have to be exact, only a maximum is needed, so adding up the space required for all the arguments and allocating that much stack space works just fine, there's no need to subtract the size for any argument passed in registers.
Upvotes: 6
Views: 250
Reputation: 299265
I don't believe the encoding format is publicly documented. You can work out most of it from the ASTContext source. Mattt's Type Encodings blog post sums it up well:
So what do we gain from our newfound understanding of Objective-C Type Encodings? Honestly, not that much (unless you’re doing any crazy metaprogramming).
But as we said from the very outset, there is wisdom in the pursuit of deciphering secret messages.
And I agree, it's a worthy pursuit. Just don't expect this to be all that useful in Swift.
To answer this specific situation:
v32@0:8@"NSString"16@?<v@?@"NSString">24
As you know from your previous research, the numbers don't mean much, so we won't worry about those.
The syntax @"..."
is an object with the class name given in quotes. You can find that in the Type::ObjCObjectPointer
case:
8501 S += '@';
8502 if (OPT->getInterfaceDecl() &&
8503 (FD || Options.EncodingProperty() || Options.EncodeClassNames())) {
8504 S += '"';
8505 S += OPT->getInterfaceDecl()->getObjCRuntimeNameAsString();
8506 for (const auto *I : OPT->quals()) {
8507 S += '<';
8508 S += I->getObjCRuntimeNameAsString();
8509 S += '>';
8510 }
8511 S += '"';
8512 }
The syntax @?<...>
is a block pointer, as detailed in the Type::BlockPointer
case:
8401 case Type::BlockPointer: {
8402 const auto *BT = T->castAs<BlockPointerType>();
8403 S += "@?"; // Unlike a pointer-to-function, which is "^?".
8404 if (Options.EncodeBlockParameters()) {
8405 const auto *FT = BT->getPointeeType()->castAs<FunctionType>();
8406
8407 S += '<';
8408 // Block return type
8409 getObjCEncodingForTypeImpl(FT->getReturnType(), S,
8410 Options.forComponentType(), FD, NotEncodedT);
8411 // Block self
8412 S += "@?";
8413 // Block parameters
8414 if (const auto *FPT = dyn_cast<FunctionProtoType>(FT)) {
8415 for (const auto &I : FPT->param_types())
8416 getObjCEncodingForTypeImpl(I, S, Options.forComponentType(), FD,
8417 NotEncodedT);
8418 }
8419 S += '>';
8420 }
8421 return;
8422 }
What this is encoding is the following:
- (void)m:(NSString *)s completion:(void (^)(NSString *))c;
This is how async
methods are translated to ObjC: a completion handler is added that accepts the return value.
For fun, you can extend this to an async throws
function:
func DebugLog2(message: String) async throws -> String
And the result will be:
v32@0:8@"NSString"16@?<v@?@"NSString"@"NSError">24
Which is equivalent to:
- (void)m:(NSString *)s completion:(void (^)(NSString *, NSError *))c;
(I should be clear when I say "equivalent" here, I don't mean it would literally be the same as calling method_getTypeEncoding
on these methods. method_getTypeEncoding
doesn't return extended encodings. You'd get v32@0:8@16@?24
for both of these: a void-returning method that takes an object (@) and a block (@?). But these are the methods being described by the Swift encodings.)
As a side note, NSMethodSignature can definitely parse the result. You just changed it to an invalid string (you removed the return type):
const char *types = "v32@0:8@\"NSString\"16@?<v@?@\"NSString\">24";
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:types];
printf("%s\n", [signature getArgumentTypeAtIndex:3]);
// Outputs: @?<v@?@"NSString">
Upvotes: 4