-
-
Save MarkVillacampa/7379799 to your computer and use it in GitHub Desktop.
#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); | |
} | |
} |
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_getMethodTypeEncoding
cannot properly read the method description from dynamically created protocols.
The remaining two questions are:
- Why did the WebKit guys use
_protocol_getMethodTypeEncoding
instead ofprotocol_getMethodDescription
followed by a call totypes
? - 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.
Did you get any info on this from Apple or the webkit developers?
Just filed a Webkit bug regarding this: https://bugs.webkit.org/show_bug.cgi?id=126121
@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:
- 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). - 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.
fwiw this is one of the biggest reasons I ditched JS and went with Lua for my app's extension stuff
First things first: L41 is wrong. Where it says
@selector(method)
should beNSSelectorFromString(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.
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 fromsignatureWithObjcClasses
, which is a parameter to this function.Third stop: JSWrapperMap.mm, specifically, the block inside the
copyMethodsToObject
function. There is no direct reference toobjCCallbackFunctionForInvocation
, but the backtrace is lying here, sinceobjCCallbackFunctionForMethod
seems to be tail-call-optimized intoobjCCallbackFunctionForInvocation
.Let’s see that optimization: Going back to ObjcRuntimeExtras.mm. The parameter
types
is used to create theNSInvocation
, 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 returnNULL
.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 inWebKit
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).