Rolf Bjarne Kvinge
Rolf Bjarne Kvinge

Reputation: 19335

What's the format for `objc_method_description.types` for swift blocks?

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:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

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":

https://github.com/apple/swift/blob/c2fc7ee9e72e9a39854548e3202c667e4934dc65/test/IRGen/objc_methods.swift#L13-L14

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

Answers (1)

Rob Napier
Rob Napier

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

Related Questions