Skip to content

Instantly share code, notes, and snippets.

@MarkVillacampa
Last active December 27, 2015 19:49
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarkVillacampa/7379799 to your computer and use it in GitHub Desktop.
Save MarkVillacampa/7379799 to your computer and use it in GitHub Desktop.
Trying to add methods at runtime for the JSExport protocol in JavaScriptCore. As-is, the method is not called from Javascript. Uncomment lines 7-8 and the end of 11, comment line 65, and run. The method is now called from JavaSript. Any idea why?
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
#import <objc/runtime.h>
const char *_protocol_getMethodTypeEncoding(Protocol *, SEL, BOOL isRequiredMethod, BOOL isInstanceMethod);
//@protocol MyProtocol <JSExport>
// -(void)one:(id)one;
//@end
@interface MyClass : NSObject //<MyProtocol>
-(void)one:(id)one;
+(Protocol*)getProtocol:(NSString*)protocol;
+(void)addProtocol:(NSString*)protocol extendingProtocol:(NSString*)extends withMethods:(NSArray*)methods;
@end
@implementation MyClass : NSObject
-(void)one:(id)one
{
NSLog(@"Hello!");
}
+(void)addProtocol:(NSString*)protocol extendingProtocol:(NSString*)extends withMethods:(NSArray*)methods;
{
Protocol *prot = [self getProtocol:protocol];
if (extends != nil)
{
protocol_addProtocol(prot, objc_getProtocol([extends UTF8String]));
}
if (methods != nil)
{
for(NSString *method in methods)
{
bool instance = true;
Method m = class_getInstanceMethod(self, NSSelectorFromString(method));
if (m == nil)
{
instance = false;
m = class_getClassMethod(self, NSSelectorFromString(method));
}
const char *types = method_getTypeEncoding(m);
protocol_addMethodDescription(prot, NSSelectorFromString(method), types, true, instance);
}
}
objc_registerProtocol(prot);
class_addProtocol([self class], prot);
}
+(Protocol*)getProtocol:(NSString*)protocol
{
Protocol *prot = objc_getProtocol([protocol UTF8String]);
if (prot == nil)
{
prot = objc_allocateProtocol([protocol UTF8String]);
}
return prot;
}
@end
int main(int argc, char *argv[]) {
@autoreleasepool {
@protocol(JSExport); // Stub needed to "see" the protocol at runtime.
[MyClass addProtocol:@"MyProtocol" extendingProtocol:@"JSExport" withMethods: @[@"one:"]];
JSContext *context = JSContext.new;
// MyClass *myclass = MyClass.class;
// context[@"Myclass"] = myclass;
// [context evaluateScript:@"Myclass.one()"];
NSLog(@"Conforms to JSExport: %d", class_conformsToProtocol(NSClassFromString(@"MyClass"), objc_getProtocol([@"JSExport" UTF8String])));
NSLog(@"Conforms to MyProtocol: %d", class_conformsToProtocol(NSClassFromString(@"MyClass"), objc_getProtocol([@"MyProtocol" UTF8String])));
uint *outCount;
protocol_copyMethodDescriptionList(objc_getProtocol([@"MyProtocol" UTF8String]), true, true, &outCount);
protocol_copyMethodDescriptionList(objc_getProtocol([@"MyProtocol" UTF8String]), true, true, &outCount);
NSLog(@"Types: %s", protocol_getMethodDescription(NSProtocolFromString(@"MyProtocol"),NSSelectorFromString(@"one:"), true, true).types);
NSLog(@"Types internal: %s", _protocol_getMethodTypeEncoding(NSProtocolFromString(@"MyProtocol"),NSSelectorFromString(@"one:"), true, true));
NSLog(@"Number of methods in MyProtocol: %i", outCount);
}
}
@drodriguez
Copy link

First things first: L41 is wrong. Where it says @selector(method) should be NSSelectorFromString(method).

And then the problem: I got the same behaviour as you. Creating the protocol dynamically I get a segmentation fault. This is the backtrace at the moment of the segmentation fault.

* thread #1: tid = 0x71c9fb, 0x00007fff9401d2c3 JavaScriptCore`ResultTypeDelegate::ResultType parseObjCType<ResultTypeDelegate>(char const*&) + 19, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff9401d2c3 JavaScriptCore`ResultTypeDelegate::ResultType parseObjCType<ResultTypeDelegate>(char const*&) + 19
    frame #1: 0x00007fff9401cdfd JavaScriptCore`objCCallbackFunctionForInvocation(JSContext*, NSInvocation*, CallbackType, objc_class*, char const*) + 45
    frame #2: 0x00007fff940929d8 JavaScriptCore`___ZL19copyMethodsToObjectP9JSContextP10objc_classP8ProtocolaP7JSValueP19NSMutableDictionary_block_invoke + 504
    frame #3: 0x00007fff9409262b JavaScriptCore`copyMethodsToObject(JSContext*, objc_class*, Protocol*, signed char, JSValue*, NSMutableDictionary*) + 475
    frame #4: 0x00007fff94092137 JavaScriptCore`__69-[JSObjCClassInfo allocateConstructorAndPrototypeWithSuperClassInfo:]_block_invoke + 423
    frame #5: 0x00007fff9401c841 JavaScriptCore`forEachProtocolImplementingProtocol(objc_class*, Protocol*, void (Protocol*) block_pointer) + 257
    frame #6: 0x00007fff9401ae40 JavaScriptCore`-[JSObjCClassInfo allocateConstructorAndPrototypeWithSuperClassInfo:] + 1056
    frame #7: 0x00007fff9401aa00 JavaScriptCore`-[JSObjCClassInfo initWithContext:forClass:superClassInfo:] + 272
    frame #8: 0x00007fff9401a880 JavaScriptCore`-[JSWrapperMap classInfoForClass:] + 224
    frame #9: 0x00007fff9401a5c3 JavaScriptCore`-[JSWrapperMap jsWrapperForObject:] + 403
    frame #10: 0x00007fff9401a400 JavaScriptCore`-[JSContext(Internal) wrapperForObjCObject:] + 64
    frame #11: 0x00007fff9401949c JavaScriptCore`objectToValueWithoutCopy(JSContext*, objc_object*) + 1084
    frame #12: 0x00007fff94018cab JavaScriptCore`objectToValue(JSContext*, objc_object*) + 75
    frame #13: 0x00007fff9401a36a JavaScriptCore`-[JSValue setValue:forProperty:] + 138
    frame #14: 0x0000000100001b2c JavaScriptCore`main(argc=1, argv=0x00007fff5fbff6e0) + 396 at JavaScriptCore.m:75
    frame #15: 0x00007fff962565fd libdyld.dylib`start + 1

Luckily for us, a lot of those parts are actually open source. Our first stop is at ObjcRuntimeExtras.h. I don’t really know what exactly fails here (it may be the assert at the top of the method), but with the debugger I confirmed that the only parameter seems to be NULL.

Second stop: ObjcRuntimeExtras.mm. There are two invocations to parseObjCType, but let’s suppose is the first one. position, the parameter, comes from signatureWithObjcClasses, which is a parameter to this function.

Third stop: JSWrapperMap.mm, specifically, the block inside the copyMethodsToObject function. There is no direct reference to objCCallbackFunctionForInvocation, but the backtrace is lying here, since objCCallbackFunctionForMethod seems to be tail-call-optimized into objCCallbackFunctionForInvocation.

Let’s see that optimization: Going back to ObjcRuntimeExtras.mm. The parameter types is used to create the NSInvocation, but instead of forwarding it to the next function, the last argument is actually _protocol_getMethodTypeEncoding(protocol, sel, YES, isInstanceMethod).

That function starting by an underscore is not a good sign. As pointed at the bottom of ObjcRuntimeExtras.h is actually a private function. But obviously the rules about private APIs are not made for Apple. I don’t really understand why they need to obtain the types again using this function, because the argument comes from the very public protocol_copyMethodList.

So, what does _protocol_getMethodTypeEncoding looks like, well, at least in 10.9 it looks like this (sorry I cannot link to an specific line, just search for the method name). There you can see the following comment: “Returns nil if the compiler did not emit any extended @encode data.”. That looks like your suspect. The compiler could not emit any “extended @encode data” because the compiler didn’t know about your protocol. When you uncomment the protocol and use it in your code (as part of an interface declaration or using @protocol(...)), the binary knows about the protocol and that function doesn’t return NULL.

In my opinion, you are out of luck if you need to define dynamic protocols and methods for JSExport. I don’t know if it is a bug in WebKit implementation or not. I recommend you to open an issue with them, because they will be able to answer a lot better why they need to ask for the type encoding again to that private function (when they already have it as an argument).

@MarkVillacampa
Copy link
Author

Thanks @drodriguez!

I originally wrote this in Ruby (Rubymotion) and ended up translating it to Objc to see it if was a RM flaw or a limitation of the Objc runtime. Turns out it was the 2nd.

One thing I didn't mention was that if you change line 41 to make the method in the protocol not required (changing true to false) there is no segfault and the method gets correctly added (the debug messages on the bottom output exactly the same as if you uncomment the protocol definition etc.)

protocol_addMethodDescription(prot, @selector(method), types, false, instance);

Apparently JavaScriptCore, again for a un known reason, requires the method to be "required" by the protocol. If you add @optional just before the declaration of the protocol, JSC doesn't take it into account either.

I suspect the first and last line here are responsible for that:
https://github.com/WebKit/webkit/blob/1e3d59b81f4029938c9f4882020849182a109fa5/Source/JavaScriptCore/API/ObjCCallbackFunction.mm#L679-L690

I've modified the gist adding two type logs for the method, one using the public protocol_copyMethodDescriptionList and another with the private _protocol_getMethodTypeEncoding. The unsurprising results are as follows:

# Dynamic 

2013-11-10 15:56:28.575 acasfdas[62448:303] Conforms to JSExport: 1
2013-11-10 15:56:28.576 acasfdas[62448:303] Conforms to MyProtocol: 1
2013-11-10 15:56:28.577 acasfdas[62448:303] Types: v24@0:8@16
2013-11-10 15:56:28.577 acasfdas[62448:303] Types internal: (null)
2013-11-10 15:56:28.578 acasfdas[62448:303] Number of methods in MyProtocol: 1

# Static

2013-11-10 15:54:47.167 acasfdas[62431:303] Conforms to JSExport: 1
2013-11-10 15:54:47.168 acasfdas[62431:303] Conforms to MyProtocol: 1
2013-11-10 15:54:47.168 acasfdas[62431:303] Types: v24@0:8@16
2013-11-10 15:54:47.170 acasfdas[62431:303] Types internal: v24@0:8@16
2013-11-10 15:54:47.170 acasfdas[62431:303] Number of methods in MyProtocol: 1

So apparently, _protocol_getMethodTypeEncodingcannot properly read the method description from dynamically created protocols.

The remaining two questions are:

  • Why did the WebKit guys use _protocol_getMethodTypeEncoding instead of protocol_getMethodDescriptionfollowed by a call to types?
  • Is there a bug in the internal _protocol_getMethodTypeEncoding?

I guess I'll have to file some tickets, and come back with some answers in (hopefully only) a few months from now. Until then I will probably do some more detective work.

@sierraw5
Copy link

Did you get any info on this from Apple or the webkit developers?

@MarkVillacampa
Copy link
Author

Just filed a Webkit bug regarding this: https://bugs.webkit.org/show_bug.cgi?id=126121

@interstateone
Copy link

@MarkVillacampa I commented on that bug but missed hitting "reply" so thought I'd follow up here as well. I did some investigating and it looks like this won't be something that would get changed in JSC itself because it would have bad side effects. I'll note I'm not involved with WebKit, I've only done some testing and prodding of my own. To answer your two other questions:

  1. Because using method_getTypeEncoding with dynamic protocols doesn't have the extended type encoding that _protocol_getMethodTypeEncoding does and that's needed to handle data types when moving to/from ObjC to JS (from my experience specifically with JSValue method arguments).
  2. Doesn't seem like it. It returns null for dynamic protocols because it's only using the compile-time information.

It's certainly possible to build your own JSC to put into an app though, and projects like this make that easier.

@sdegutis
Copy link

fwiw this is one of the biggest reasons I ditched JS and went with Lua for my app's extension stuff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment