I heard before that DynamicCocoa of didi is based on JavaScriptCore, I have been expecting to see their real implementation, but maybe later due to company secrets, I can’t open source any more.
With the opportunity of studying JavaScriptCore recently, I decided to make a simple iOS dynamic executor toy in my spare time using the JavaScript knowledge I learned in the last day or two.
1: I heard that Didi has developed a set of intermediate language interpreter based on LLVM Backend. I don’t know which one is used in the end. The LLVM IR interpreter, however, is somewhat interesting.
Digression 2: I did this not to do iOS dynamics, because XXXXXXX. I just want to see some implementation of JavaScriptCore.
The effect
A Gif must be the best way to show my toys, please look at them:
Pre-knowledge point
Before we implement our actuator, we still need to know a little bit about the prior knowledge.
JSWrapper Object
As we all know, many objective-C types cannot be used directly in the context of JavaScript and need to be wrapped in JSValue. The specific type conversion is shown in the following figure:
The basic transformations above are easy to understand, the only thing we need to pay attention to is the Wrapper Object. What is the Wrapper Object?
Here’s an example:
self.context[@"a"] = [CustomObject new]
Copy the code
The above code injects an instance of our custom type CustomObject into the JavaScript runtime as variable name A. But how does she know our definition, how does she know if we can call a particular method?
By default, the JS runtime will simply synchronize the OC init initialization method and class inheritance relationship to the JS environment (if there is a JSExport we’ll talk about below), which will then wrap a JSWrapperValue for use in the JS environment. When the JAVASCRIPT environment calls OC and involves this object, JavaScriptCore automatically unpacks it and restores it to the original OC object type.
- (JSValue *)jsWrapperForObject:(id)object { JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object); if (jsWrapper) return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context]; // pay attention to!!!!!!!!!!!!!!!!!! JSValue *wrapper; if (class_isMetaClass(object_getClass(object))) wrapper = [[self classInfoForClass:(Class)object] constructor]; else { JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]]; wrapper = [classInfo wrapperForObject:object]; } JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]); jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec); m_cachedJSWrappers.set(object, jsWrapper); return wrapper; }Copy the code
- The overall analysis is based on a cache to determine whether a particular object or type has been built
Wrapper Object
If not, build it as follows:
JSClassDefinition definition;
definition = kJSClassDefinitionEmpty;
definition.className = className;
m_classRef = JSClassCreate(&definition);
[self allocateConstructorAndPrototypeWithSuperClassInfo:superClassInfo];
Copy the code
- Nothing special, OC objects create corresponding JS objects, type to type.
- The OC type inheritance relationship is built in JS by setting Constructor and Prototype to simple JavaScript Prototype chain inheritance.
JSExport protocol & JSExportAs
The JSExport Protocol is essentially just a Protocol flag that lets JavaScriptCore load classes marked with this special flag for registration and initialization in a specific manner.
As we mentioned earlier, by default, JavaScriptCore creates a default Wrapper Object, but this Object is nothing more than a Constructor to a specially formatted command:
[NSString stringWithFormat:@"%sConstructor", className]
If we need to inject methods in OC environment into JS environment, we need to use JSExport protocol. This protocol will be processed according to the following logic at runtime, such as method and attribute injection:
Check the init method cluster method and provide a reasonable __block HashMap<String, Protocol *> initTable based on this law; Protocol *exportProtocol = getJSExportProtocol(); for (Class currentClass = cls; currentClass; currentClass = class_getSuperclass(currentClass)) { forEachProtocolImplementingProtocol(currentClass, exportProtocol, ^(Protocol *protocol) { forEachMethodInProtocol(protocol, YES, YES, ^(SEL selector, const char*) { const char* name = sel_getName(selector); if (! isInitFamilyMethod(@(name))) return; initTable.set(name, protocol); }); }); } for (Class currentClass = cls; currentClass; currentClass = class_getSuperclass(currentClass)) { __block unsigned numberOfInitsFound = 0; __block SEL initMethod = 0; __block Protocol *initProtocol = 0; __block const char* types = 0; forEachMethodInClass(currentClass, ^(Method method) { SEL selector = method_getName(method); const char* name = sel_getName(selector); auto iter = initTable.find(name); if (iter == initTable.end()) return; numberOfInitsFound++; initMethod = selector; initProtocol = iter->value; types = method_getTypeEncoding(method); }); if (! numberOfInitsFound) continue; if (numberOfInitsFound > 1) { NSLog(@"ERROR: Class %@ exported more than one init family method via JSExport. Class %@ will not have a callable JavaScript constructor function.", cls, cls); break; } JSObjectRef method = objCCallbackFunctionForInit(context, cls, initProtocol, initMethod, types); return [JSValue valueWithJSValueRef:method inContext:context]; }Copy the code
Protocol *exportProtocol = getJSExportProtocol(); forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){ copyPrototypeProperties(m_context, m_class, protocol, prototype); copyMethodsToObject(m_context, m_class, protocol, NO, constructor); });Copy the code
As for JSExportAs, there is a simple name mapping, after all, JS function parameters and OC are very different:
static NSMutableDictionary *createRenameMap(Protocol *protocol, BOOL isInstanceMethod) { NSMutableDictionary *renameMap = [[NSMutableDictionary alloc] init]; forEachMethodInProtocol(protocol, NO, isInstanceMethod, ^(SEL sel, const char*){ NSString *rename = @(sel_getName(sel)); NSRange range = [rename rangeOfString:@"__JS_EXPORT_AS__"]; if (range.location == NSNotFound) return; NSString *selector = [rename substringToIndex:range.location]; NSUInteger begin = range.location + range.length; NSUInteger length = [rename length] - begin - 1; NSString *name = [rename substringWithRange:(NSRange){ begin, length }]; renameMap[selector] = name; }); return renameMap; }Copy the code
The implementation process
With that said, let’s look at the implementation process:
Classes, instances, and methods
In my view, three elements are essential for a dynamic execution environment:
Classes (including metaclasses), instance objects, and methods.
Based on our analysis of the WrapperObject above, we can build a special type of WrapperObject to wrap these three elements. We don’t need to talk about the details, but we recommend you to think about it by yourself, basically similar to the steps I did for JSWrapperObject above.
In addition to the above three elements, we need to define a global variable, WZGloablObject, which intercepts top-level access to properties.
With this design, you can think for yourself about how you would proceed if you were doing it, and the article will be published next week along with the code.
Choose debug
For those of you who have used Cycript in reverse order, Cycript has a very convenient debug function: Choose. This function can quickly help us to return all objects on the heap based on the class name.
Such useful functionality had to be provided, and I basically copied the Cycript implementation directly. The code is clear and basically self-explanatory. The core basically iterates through each malloc_zone and determines whether or not the retrieved data is of the desired type based on the retrieved VMAddress_range.
Zone for (unsigned I = 0; i ! = size; ++i) { const malloc_zone_t * zone = reinterpret_cast<const malloc_zone_t *>(zones[i]); if (zone == NULL || zone->introspect == NULL) continue; zone->introspect->enumerator(mach_task_self(), &choice, MALLOC_PTR_IN_USE_RANGE_TYPE, zones[i], &read_memory, &choose_); } // Check the object for (unsigned I = 0; i < count; ++i) { vm_range_t &range = ranges[i]; void * data = reinterpret_cast<void *>(range.address); size_t size = range.size; if (size < sizeof(ObjectStruct)) continue; uintptr_t * pointers = reinterpret_cast<uintptr_t *>(data); #ifdef __arm64__ Class isa = (__bridge Class)((void *)(pointers[0] & 0x1fffffff8)); #else Class isa = reinterpret_cast<Class>(pointers[0]); #endif std::set<Class>::const_iterator result(choice->query_.find(isa)); if (result == choice->query_.end()) continue; size_t needed = class_getInstanceSize(*result); size_t boundary = 496; #ifdef __LP64__ boundary *= 2; #endif if ((needed <= boundary && (needed + 15) / 16 * 16 ! = size) || (needed > boundary && (needed + 511) / 512 * 512 ! = size)) continue; choice->result_.insert((__bridge id)(data)); }Copy the code
But here a lot of 511, 512 numbers constitute the formula, to be honest, I do not understand, if you know the big guy please inform me.
Type conversion
The first thing to remember is that JavaScript’s basic types are as follows:
-string, -number, -boolean, -array, -object, -null, -undefinedCopy the code
So we just need to convert according to the corresponding, as follows:
- JS string <-> NSString
- Digital < – > NSNumber
- Array < – > NSArray
- Null <-> NSNull
- Undefined <-> Void (only if a value is returned, otherwise an exception is thrown)
As an aside, there’s nothing in JavaScript that distinguishes between integer and floating point types, so we can mindlessly construct an NSNumber from a double, right
Finally, the handling of object types:
In JavaScript, any object can simply be thought of as a wrapper containing properties (methods), as follows:
var a = {x:10, y:100};
Copy the code
Therefore, we should pay special attention to the following points when converting types:
- Is this the same class, instance, or method we just mentioned, or is it fetched from the JSWrapperObject before it enters the Objective-C execution context?
- Is this object a struct of a particular type, if it is, we’re going to convert it to a struct, for example
CGRect
Or something like that, which requires a special conversion - Can be directly converted to a particular type of object, for example
Date <-> NSDate
The conversion. - Finally, convert its traversable attribute and its corresponding attribute value to
NSDictionary
. - Of course, don’t forget to pay attention to recursion.
Calling Convention
There is no further discussion of Calling Convention in this article, but interested readers can refer to the Zhihu column I wrote with my colleagues on iOS Debugging
To rephrase it simply:
When a function is called, the parameters of the function can be passed either on the stack or in registers. The parameters can be pushed from left to right or from right to left. After a function is called, the parameters can be pushed off the stack. If the caller and the called (the function itself) do not follow the same convention, there are so many differences that the function call cannot be completed. This common Convention that both parties must follow is called the Calling Convention, which specifies the order and manner in which arguments are passed, and how the stack is maintained.
Since libffi was already written by a big name in the industry, we didn’t have to reinvent the wheel, we could just use it. If you really want to understand how this works, you can also refer to my article to analyze the implementation process of objc_msgSend.
other
In order to be lazy, I implemented these effects directly in JavaScript. In theory, if I fully implement the compile front end, build an abstract syntax tree to analyze the execution context, and convert objective-C code to JavaScript, then I can implement dynamic execution of Objective-C code. (Of course, it’s still a smoke screen.)
The faster way, which is by no means guaranteed to be correct, is to just call JSPatchConvertor, hahaha.