Skip to content

Instantly share code, notes, and snippets.

@MarkVillacampa
Last active December 27, 2015 19:49
Show Gist options
  • 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);
}
}
@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