朱雀-基于LLVM的Objective-C解释器的基本实现
0 前言
朱雀是中国古代神兽之一,以此来命名这个项目,希望大家可以在闲暇之余可以多了解一些古代的神话故事,会有别样的乐趣和收获。
这应该是一篇比较长的文章,来尝试描述清楚这个项目的实现方案。不过受限于作者本身的技术和理解能力,本文可能会存在很多值得商榷甚至是完全错误的地方,希望尽量不会给读者带来误区,也十分欢迎相关专家批评指正。当然,阅读这篇文章也需要一定的OC语言和iOS开发基础,一些基础的概念没有做过多的赘述。
本文也不讨论任何有关开发模式,应用审核,以及实际应用等问题,希望只可以单独的作为一个技术项目来进行研究和拓展。
目前朱雀对于OC的语言特性以及数据类型的支持并不完整,文中的代码目前支持部分OC代码和数据类型,供阐述原理以及开发测试使用,后续会列出目前暂不支持的用法和数据类型。同时代码中可能有一些很明显的内存泄漏,有些是有意为之,在下面的多线程部分会有说明。
1 概述
众所周知OC是一门所谓编译型的语言,是C语言的一个超集。朱雀项目的目的是支持在iOS模拟器和真机的环境下解释执行OC代码。熟悉Flutter的朋友们应该对Dart语言有一些了解,Dart可以支持AOT和JIT等不同的执行模式。但在iOS环境下,受限于iOS对于可写可执行内存的管控,除去JSCore等苹果自己的框架之外,JIT也是无法实现的。朱雀项目实现了一个简单的解释器,以纯解释执行的模式来执行对应的OC代码。
之所以想实现一个解释器来解释执行OC代码,主要的好处在于两点。一是大幅降低了开发成本和对技术栈的要求,也免去了不同语言转换成OC的复杂度。第二点比较关键,在于保持了内存管理的语义一致性。众所周知OC采用了引用计数的方式来管理内存,而IR代码中编译器引入的ARC相关的指令可以被朱雀完美支持,会在执行时使用libffi调用runtime中对应的函数,下面的 2.5 ARC 一节也会对此做具体的说明。
在思考朱雀的技术方案时,首先想到的就是LLVM,因为一个人从0到1实现一个完整的OC解释器不太现实,肯定要借助于一些已有的框架。
Mac/iOS的编译链使用了Clang&LLVM,LLVM是Low Level Virtual Machine的缩写。LLVM有一个中间语言的概念,也就是IR,是编译过程的中间产物。大家听起来会熟悉一些的Bitcode是同一个概念。所以问题来了,既然LLVM本身就是一个“VM”,而这个VM所使用的IR又是OC代码编译过程中的产物,那么可不可以使用LLVM作为一个解释器来解释执行IR代码,从而达到动态执行一段逻辑的目的呢?答案是肯定的,但问题也有很多。
LLVM本身有一个简单的解释器的实现,可以解释执行C代码生成的部分IR代码,并且可以链接外部的动态库,使用libffi来进行外部函数的调用。但这个解释器对于OC来说是一个不完整的实现,IR代码中涉及到OC Runtime的部分都没有支持,这些缺失的部分需要我们补齐实现。朱雀的实现可以理解为是在LLVM的基础上打了很多补丁,来支持OC的解释执行。
2 应用
朱雀目前对外暴露了一个函数用于调用。参数中的filePath是要加载的IR文件的路径,invocation可以是runtime在消息转发过程中构造的,当然也可以手动构造并传入。invocation中包含了target,selector以及参数等信息。IR文件中对应的Function执行结束之后会设置invocation的return value,用于处理返回值。
void runIRCode(NSString *filePath, NSInvocation *invocation)
{
StringRef InputFile = StringRef([filePath cStringUsingEncoding:NSUTF8StringEncoding]);
static LLVMContext Context;
SMDiagnostic Err;
std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, Context);
if (!Owner.get()){
reportError(Err, "create module");
}
EngineBuilder builder(std::move(Owner));
builder.setEngineKind(EngineKind::Interpreter).setErrorStr(NULL);
ExecutionEngine *engine = builder.create();
engine->currentModuleFilePath = std::string([filePath cStringUsingEncoding:NSUTF8StringEncoding]);
NSString *methodName = NSStringFromSelector(invocation.selector);
NSString *className = NSStringFromClass([invocation.target class]);
NSString *functionName = [NSString stringWithFormat:@"%@%@%@%@%@", @"�-[", className, @" ", methodName ,@"]"];
Function *function = engine->FindFunctionNamed(StringRef([functionName cStringUsingEncoding:NSUTF8StringEncoding]));
std::vector<GenericValue> params;
NSMethodSignature *signature = invocation.methodSignature;
int argCount = (int)signature.numberOfArguments;
for (int i = 0;i < signature.numberOfArguments;i++){
GenericValue value;
const char *type = [signature getArgumentTypeAtIndex:i];
NSString *typeString = [[NSString alloc] initWithUTF8String:type];
void *argument = nil;
[invocation getArgument:&argument atIndex:i];
if ([typeString containsString:@"i"]){
int intResult;
memcpy(&intResult, &argument, sizeof(int));
value.IntVal = APInt(32, intResult);
} else if ([typeString containsString:@"i"]){
int intResult;
memcpy(&intResult, &argument, sizeof(int));
value.IntVal = APInt(32, intResult);
} else {
value.PointerVal = argument;
}
params.push_back(value);
}
GenericValue GV = engine->runFunction(function, params);
const char *type = signature.methodReturnType;
NSString *typeString = [[NSString alloc] initWithUTF8String:type];
if ([typeString isEqualToString:@"i"] || [typeString isEqualToString:@"I"]){
int intResult = GV.IntVal.getZExtValue();
[invocation setReturnValue:&intResult];
} else if ([typeString isEqualToString:@"l"] || [typeString isEqualToString:@"L"]){
long longResult = GV.IntVal.getZExtValue();
[invocation setReturnValue:&longResult];
} else if ([typeString isEqualToString:@"f"]){
float floatResult = GV.FloatVal;
[invocation setReturnValue:&floatResult];
} else if ([typeString isEqualToString:@"d"]){
double doubleResult = GV.DoubleVal;
[invocation setReturnValue:&doubleResult];
} else if ([typeString isEqualToString:@"B"]){
BOOL boolResult = GV.IntVal.getBoolValue() ? YES : NO;
[invocation setReturnValue:&boolResult];
} else if ([typeString containsString:@"*"]){
void *result = GV.PointerVal;
[invocation setReturnValue:&result];
} else if ([typeString containsString:@"@"]){
void *result = GV.PointerVal;
[invocation setReturnValue:&result];
} else if ([typeString containsString:@":"]){
void *result = GV.PointerVal;
[invocation setReturnValue:&result];
} else if ([typeString containsString:@"#"]){
void *result = GV.PointerVal;
[invocation setReturnValue:&result];
} else if ([typeString containsString:@"v"]){
// do nothing
} else {
NSLog(@"typeString:%@", typeString);
report_fatal_error("Current not support type");
}
}
下面是开发过程用于测试的代码,calculateWithA:andB:方法可以被朱雀解释执行并且完成同样的逻辑。
#import "TestClass2.h"
#import <objc/message.h>
#import "TestView.h"
@interface TestClass2()
@property (nonatomic) int testInt;
@property (nonatomic, strong) UIImageView *view;
@end
@implementation TestClass2
- (void)tap
{
printf("tap called\n");
}
- (void)testAddMethod
{
printf(":::testAddMethod called\n");
}
+ (void)testClassMethod
{
printf("testClassMethod called\n");
}
- (int)calculateWithA:(int)a andB:(int)b
{
printf("%d\n", self.testInt);
self.testInt = 10;
printf("%d\n", self.testInt);
self.view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 300, 200, 200)];
self.view.image = [UIImage imageNamed:@"1024"];
[[UIApplication sharedApplication].keyWindow addSubview:self.view];
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
[self.view addGestureRecognizer:tap];
int __block h = 3333;
for (int i = 0;i < 1;i++){
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
h ++;
printf(":::change a in block:%d\n", 808);
});
}
int __block m = 10;
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
m = 88;
printf(":::change a in block:%d\n", m);
});
__block NSString * str = @"hello1111111111111111";
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
str = @"change hello11111111111111111111111";
//str = [NSString stringWithFormat:@"change hello11111111111111111111111"]
printf(":::change a in block:%s\n", [str cStringUsingEncoding:NSUTF8StringEncoding]);
});
__block NSNumber * num = [NSNumber numberWithInt:77];
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
num = [NSNumber numberWithFloat:50.0f];
printf(":::change a in block:%f\n", num.floatValue);
});
for (int i = 0;i < 2;i++){
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
printf(":::change a in block:%d\n", 666);
});
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
printf(":::change a in block:%d\n", 888);
});
}
NSString *s = @"hello";
s = [s performSelector:@selector(uppercaseString)];
printf("uppercaseString:%s\n", [s cStringUsingEncoding:NSUTF8StringEncoding]);
[self performSelector:@selector(testPerform) withObject:s afterDelay:5];
TestView *view = [[TestView alloc] initWithFrame:CGRectMake(100.0f, 100.0f, 200.0f, 200.0f)];
view.backgroundColor = [UIColor redColor];
view.frame = CGRectMake(100.0f, 100.0f, 300.0f, 100.0f);
[[UIApplication sharedApplication].keyWindow addSubview:view];
[view testClass:[NSString class] b:YES c:'f' cp:"hello cp"];
[view testBlock:^(char input) {
printf("testArgument called with input:%c\n", input);
}];
__weak id weakSelf = self;
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://www.mi.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
printf(":::nsurlsession callback called\n");
NSString *rStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
printf(":::url session response:%s\n", rStr.UTF8String);
NSLog(@"weakSelf:%@", weakSelf);
}] resume];
NSTimer *timer = [NSTimer timerWithTimeInterval:30.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
printf("test timer\n");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
int o = 300;
void(^testArgument)(int) = ^(int input){
printf("testArgument called with input:%d\n", input + o);
};
testArgument(456);
void(^testArgument1)(double) = ^(double input){
printf("testArgument called with input:%lf\n", input);
};
testArgument1(456.789f);
void(^testArgument2)(char) = ^(char input){
printf("testArgument called with input:%c\n", input);
};
testArgument2('e');
[self testAddMethod];
[[self class] testClassMethod];
return [NSNumber numberWithInt:33].intValue + 88 + [super calculateWithA:a andB:b];
}
@end
目前朱雀可以作为一个动态库被嵌入到iOS的工程中。
假设我们之前自己创建了一个类叫做AClass,这个类有一个实例方法叫做calculate。我们之后修改了calculate的实现,使用clang生成了对应的IR文件。在应用启动后,可以获取到这个IR文件,通过runtime可以替换掉原有calculate方法的实现,在其新的实现中可以通过朱雀解释执行IR文件中的calculate方法,从而执行我们修改后的逻辑。
3 实现
2.1 selector和class
需要注意的是OC的selector,其本质可以理解为在runtime中注册过的字符串,表示一个特定的方法名。然而LLVM解释器中使用的selector虽然字符串是等价的,但并没有在runtime中注册过,所以直接使用的话会报错,需要调用sel_registerName/sel_getUid来进行注册后使用。这两个函数的返回值是同名的但在runtime中注册过的selector。
与此同理的还有class对象,在日常的开发过程中,一般一个m文件会包含一个类的实现,然后会被编译为一个IR文件。LLVM解释器加载这个文件后会生成一个Module,在这个Module内部使用的该类的class对象只是LLVM自己生成的一个数据结构,虽然和runtime中OC类的数据结构是一样的,但这个特殊的class并没有在runtime中注册过,所以没办法使用。此处需要做一个处理,就是通过NSStringFromClass函数拿到这个class的名称,然后通过NSClassFromString函数,使用类名称拿到runtime中实际注册过的同名的类。
2.2 objc_msgSend
关于IR,有一篇很长的文档,描述了IR的各种语法和细节。
下面是截取的部分代码段:
store %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), %1** %16, align 8
%73 = load %1*, %1** %16, align 8
%74 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.11, align 8, !invariant.load !10
%75 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.13, align 8, !invariant.load !10
%76 = bitcast %1* %73 to i8*
%77 = invoke i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*, i8*)*)(i8* %76, i8* %75, i8* %74) to label %78 unwind label %177
可以看到,每一个OC方法的调用,都会被转换成objc_msgSend/objc_msgSend_stret等函数调用,下面暂时都用objc_msgSend表示此类函数。需要注意的是,不管编译时OBJC_OLD_DISPATCH_PROTOTYPES这个编译选项如何设置,生成的IR中,objc_msgSend都被定义成了可变参数的函数,只是在调用的时候做了一个cast,如上面的第6行代码所示。为了解决调用外部函数的问题,LLVM现有的解释器引入了libffi,可以动态调用外部的c函数。然而由于其解释器模块目前的设计,目前并没有在调用外部可变参函数时,将可变部分的参数类型传递过来。所以为了解决这个问题,我们有两个方案可以选择,一是通过runtime拿到可变部分的参数类型,通过libffi来进行调用;二是可以在LLVM解释器模块中内置一个objc_msgSend函数,使得所有的objc_msgSend调用都可以进入到我们自己定义的objc_msgSend函数中来,在其实现中,根据传入的参数,构造出NSInvocation对象,达到模拟方法调用的目的。这个objc_msgSend并不是需要我们实现一个等价于OC runtime的objc_msgSend,只是提供一个c函数给LLVM解释器使用,使得LLVM解释器认为这即是OC runtime的objc_msgSend,从而可以将需要真正传递给objc_msgSend参数传递进来,我们在自己的实现中可以使用这些参数完成消息发送的任务即可。为了减少复杂度,朱雀选择了第二种方案来实现。
下面是其大概的实现逻辑:
static GenericValue lle_X_objc_msgSend(FunctionType *FT,
ArrayRef<GenericValue> Args) {
id object = (__bridge id)GVTOP(Args[0]);
SEL sel = sel_registerName((char *)GVTOP(Args[1]));
/*
如果object本身是一个类对象,那么调用class方法会返回自身。所以可以确保拿到正确的类名。
*/
NSString *className = NSStringFromClass([object class]);
NSString *methodName = NSStringFromSelector(sel);
/*
首先需要区分是实例方法还是类方法,从而构造出不同的方法名在IR文件中查找。
通过object_isClass来判断一个对象是普通的对象还是一个类对象。
如果是类对象的话说明本次调用的方法是一个类方法。
*/
NSString *methodType = object_isClass(object)?@"+":@"-";
NSString *result = [NSString stringWithFormat:@"\01%@[%@ %@]", methodType, className, methodName];
/*
查找当前IR文件中是否包含该函数,如果包含则直接执行。
*/
Function *f = mainEngine->FindFunctionNamed(StringRef([result cStringUsingEncoding:NSUTF8StringEncoding]));
if (f){
StringRef InputFile = StringRef(mainEngine->currentModuleFilePath.c_str());
LLVMContext *Context = new LLVMContext();
SMDiagnostic Err;
std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, *Context);
std::string Error;
EngineBuilder builder(std::move(Owner));
builder.setEngineKind(EngineKind::Interpreter).setErrorStr(&Error);
ExecutionEngine *newEngine = builder.create();
f = newEngine->FindFunctionNamed(StringRef([result cStringUsingEncoding:NSUTF8StringEncoding]));
return newEngine->runFunction(f, Args);
}
NSMethodSignature *signature = [object methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:object];
[invocation setSelector:sel];
buildInvocation(invocation, Args);
[invocation invoke];
return buildReturnGenericValue(invocation);
}
objc_msgSend_fpret和objc_msgSend_fp2ret在arm64架构下不会使用,在x86架构下适用范围极小,暂时不作考虑。使用上述实现可以兼容objc_msgSend_stret的逻辑,不再需要额外处理。
2.3 super和objc_msgSendSuper
还需要考虑super的支持。虽然super和self的用法比较类似,但是两者并不是一个概念,反而有巨大的差距。self通常可以当作指向当前对象的一个指针来使用,而super则是一个编译器关键字,而不是一个指针指向了某个地方。类似于[super init]的方法调用会被实际转换成objc_msgSendSuper的函数调用。objc_msgSendSuper的第一个参数是名为objc_super的结构体,这个结构体也是编译器来生成的。OC runtime中objc_super的定义如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
在自定义的objc_msgSendSuper实现中,我们要做的就是调用目标对象的父类的目标方法。为了用最简单直接的方式来实现,我们还是使用了NSInvocation,但在设置invocation对象的target的时候遇到做一个tricky的处理。在调用invocation对象的invoke方法之前,首先需要将其target对象的class设置为其class的父类,然后在invoke方法调用结束后再设置回来,达到“欺骗”runtime的目的,使得父类的方法得以调用。因为在OC的消息调用或者invocation的invoke过程中,runtime 是会根据对象的isa指针,找到其所属的类,在类对象的方法列表中找到对应的方法来进行调用的。通过object_setClass改变对象的类至其父类之后,查找方法列表的过程就会从其父类开始,达到调用其父类方法的目的。
static GenericValue lle_X_objc_msgSendSuper(FunctionType *FT,
ArrayRef<GenericValue> Args) {
struct objc_super *s = (struct objc_super *)GVTOP(Args[0]);
Class sc = s->super_class;
NSString *name = NSStringFromClass(sc);
s->super_class = NSClassFromString(name);
s->super_class = class_getSuperclass(s->super_class);
Class originalClass = object_getClass(s->receiver);
object_setClass(s->receiver, s->super_class);
id object = s->receiver;
SEL sel = sel_registerName((char *)GVTOP(Args[1]));
NSMethodSignature *signature = [object methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:object];
[invocation setSelector:sel];
buildInvocation(invocation, Args);
[invocation invoke];
object_setClass(s->receiver, originalClass);
return buildReturnGenericValue(invocation);
}
当然,更为合理的方式还是通过libffi直接调用实际的objc_msgSendSuper函数。但由于实现成本比较大,目前采用了上述的实现方式。
2.4 Block
关于OC的block可以参照这篇文档。
大概的数据结构摘抄如下:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// imported variables
};
整个过程最难支持的就是block。因为系统的某些API也需要传入block类型的参数作为回调,所以我们要构造出合法的block对象来使用。LLVM的解释器其实可以生成block对象,其数据结构是合法的,但对象里所有的函数指针都是不可用的,因为其指向的是LLVM Module内的Function对象,而不是实际可以调用的函数指针。所以我们也需要使用libffi,根据Function对象中保存的参数个数和类型,来构造出合法的函数指针进行替换,从而构造出合法的block对象来使用。这样当block得到执行时,我们使用libffi生成的用于替换的函数就会得到调用,在其实现内,就可以执行其真正对应的LLVM Function,使得原有的逻辑得到执行。同理,可能会出现的copy_helper函数和dispose_helper函数也需要做同样的处理。所以需要一些全局的字典,来保存block对象和其对应的LLVM Function,以及对应的copy_helper和dispose_helper,这样才能在block被调用的时候找到实际要执行的LLVM函数。同理还需要处理__block变量会生成的byref结构体。
首先定义几个全局字典备用:
/*
用于保存block对象和其invoke函数对应的LLVM Function名称。
*/
static CFMutableDictionaryRef invokeDic;
/*
用于保存block对象和其descriptor的copy_helper函数对应的LLVM Function名称。
*/
static CFMutableDictionaryRef copyDic;
/*
用于保存block对象和其descriptor的dispose_helper函数对应的LLVM Function名称。
*/
static CFMutableDictionaryRef disposeDic;
/*
用于保存通过libffi构造出来的block的invoke函数和其对应的LLVM Function名称。
*/
static CFMutableDictionaryRef functionDic;
根据block的ABI定义这几个结构体备用:
struct Block_layout
{
void *isa;
volatile int32_t flags;
int32_t reserved;
void (*invoke)(void *);
struct Block_desc *desc;
};
struct Block_desc {
unsigned long int reserved;
unsigned long int size;
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
const char *signature;
};
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
int flags;
int size;
void (*byref_keep)(void *dst, void *src);
void (*byref_dispose)(void *);
};
下面是根据block对应的LLVM Module内的Function,使用libffi构造实际可用的函数指针的过程,同时将block和其对应的LLVM Function的映射关系保存在了上面提到的全局字典中备用。
ExecutionContext &SF = ECStack.back();
GenericValue src = getOperandValue(I.getOperand(0), SF);
struct Block_layout *block = (struct Block_layout *)src.PointerVal;
Value *v = (Value *)block->invoke;
Function *function = this->FindFunctionNamed(v->getName());
ffi_cif *cif = (ffi_cif *)malloc(sizeof(ffi_cif));
FunctionType *FTy = function->getFunctionType();
const unsigned NumArgs = function->arg_size();
ffi_type **args = (ffi_type **)malloc(sizeof(ffi_type *)*NumArgs);
for (Function::const_arg_iterator A = function->arg_begin(), E = function->arg_end();A != E; ++A) {
const unsigned ArgNo = A->getArgNo();
Type *ArgTy = FTy->getParamType(ArgNo);
args[ArgNo] = ffiTypeFor(ArgTy);
}
Type *RetTy = FTy->getReturnType();
ffi_type *rtype = ffiTypeFor(RetTy);
void (*globalForward)(void);
ffi_closure *closure = (ffi_closure *)ffi_closure_alloc(sizeof(ffi_closure), (void **)&globalForward);
if (ffi_prep_cif(cif, FFI_DEFAULT_ABI, NumArgs, rtype, args) == FFI_OK) {
if (ffi_prep_closure_loc(closure, cif, ffi_function, NULL, (void *)globalForward) != FFI_OK) {
//...handle error...
}
} else {
//...handle error...
}
block->invoke = (void (*)(void *))globalForward;
CFDictionaryAddValue(functionDic, (void *)globalForward, (void *)function);
struct Block_desc *desc = block->desc;
char *copyName = (char *)CFDictionaryGetValue(copyDic, desc);
if (copyName == nullptr){
Value *copy = (Value *)desc->copy_helper;
Value *destroy = (Value *)desc->dispose_helper;
CFDictionaryAddValue(copyDic, block, copy->getName().data());
CFDictionaryAddValue(disposeDic, block, destroy->getName().data());
desc->copy_helper = copy_helper;
desc->dispose_helper = dispose_helper;
CFDictionaryAddValue(copyDic, desc, copy->getName().data());
}
CFDictionaryAddValue(invokeDict, src.PointerVal, v->getName().data());
下面是全局的ffi_function,copy_helper和dispose_helper的实现,所有的block对象被外部invoke时都会调用到ffi_function函数。在这个函数的实现内,会通过block对象的地址,在invokeDic字典中查到其对应的LLVM Function,然后做一些必要的参数转换进行调用。copy_helper和dispose_helper的实现同理,也是在block对象做对应的操作时被调用。
static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) {
void *temp = args[0];
void *block;
memcpy(&block, temp, sizeof(void *));
StringRef InputFile = StringRef(mainEngine->currentModuleFilePath.c_str());
LLVMContext *Context = new LLVMContext();
SMDiagnostic Err;
std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, *Context);
std::string Error;
EngineBuilder builder(std::move(Owner));
builder.setEngineKind(EngineKind::Interpreter).setErrorStr(&Error);
ExecutionEngine *engine = builder.create();
char *funcName = (char *)CFDictionaryGetValue(invokeDic, block);
Function *function = engine->FindFunctionNamed(StringRef(funcName));
FunctionType *type = function->getFunctionType();
std::vector<GenericValue> Args;
for (Function::const_arg_iterator A = function->arg_begin(), E = function->arg_end(); A != E; ++A) {
FunctionType *FTy = function->getFunctionType();
const unsigned ArgNo = A->getArgNo();
Type *ArgTy = FTy->getParamType(ArgNo);
switch (ArgTy->getTypeID()) {
case Type::IntegerTyID:
switch (cast<IntegerType>(ArgTy)->getBitWidth()) {
case 8: {
int8_t number;
memcpy(&number, args[ArgNo], sizeof(int8_t));
GenericValue value;
value.IntVal = APInt(8, number);
Args.push_back(value);
}
case 16: {
int16_t number;
memcpy(&number, args[ArgNo], sizeof(int16_t));
GenericValue value;
value.IntVal = APInt(16, number);
Args.push_back(value);
}
case 32: {
int32_t number;
memcpy(&number, args[ArgNo], sizeof(int32_t));
GenericValue value;
value.IntVal = APInt(32, number);
Args.push_back(value);
}
case 64: {
long number;
memcpy(&number, args[ArgNo], sizeof(long));
GenericValue value;
value.IntVal = APInt(64, number);
Args.push_back(value);
}
}
case Type::FloatTyID: {
float number;
memcpy(&number, args[ArgNo], sizeof(float));
GenericValue value;
value.FloatVal = number;
Args.push_back(value);
}
case Type::DoubleTyID: {
double number;
memcpy(&number, args[ArgNo], sizeof(double));
GenericValue value;
value.DoubleVal = number;
Args.push_back(value);
}
case Type::PointerTyID: {
void* pointer = args[ArgNo];
void *param;
memcpy(¶m, pointer, sizeof(void *));
GenericValue value;
value.PointerVal = param;
Args.push_back(value);
}
default: break;
}
}
engine->runFunction(function, Args);
}
void copy_helper(void *dst, void *src)
{
StringRef InputFile = StringRef(mainEngine->currentModuleFilePath.c_str());
LLVMContext *Context = new LLVMContext();
SMDiagnostic Err;
std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, *Context);
std::string Error;
EngineBuilder builder(std::move(Owner));
builder.setEngineKind(EngineKind::Interpreter).setErrorStr(&Error);
ExecutionEngine *engine = builder.create();
char *funcName = (char *)CFDictionaryGetValue(copyDic, src);
Function *function = engine->FindFunctionNamed(StringRef(funcName));
FunctionType *type = function->getFunctionType();
std::vector<GenericValue> Args;
GenericValue value1;
value1.PointerVal = dst;
Args.push_back(value1);
GenericValue value2;
value2.PointerVal = src;
Args.push_back(value2);
engine->runFunction(function, Args);
funcName = (char *)CFDictionaryGetValue(invokeDic, src);
CFDictionaryAddValue(invokeDic, dst, funcName);
funcName = (char *)CFDictionaryGetValue(copyDic, src);
CFDictionaryAddValue(copyDic, dst, funcName);
funcName = (char *)CFDictionaryGetValue(disposeDic, src);
CFDictionaryAddValue(disposeDic, dst, funcName);
}
void dispose_helper(void *block)
{
StringRef InputFile = StringRef(mainEngine->currentModuleFilePath.c_str());
LLVMContext *Context = new LLVMContext();
SMDiagnostic Err;
std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, *Context);
std::string Error;
EngineBuilder builder(std::move(Owner));
builder.setEngineKind(EngineKind::Interpreter).setErrorStr(&Error);
ExecutionEngine *engine = builder.create();
char *funcName = (char *)CFDictionaryGetValue(disposeDic, block);
Function *function = engine->FindFunctionNamed(StringRef(funcName));
FunctionType *type = function->getFunctionType();
std::vector<GenericValue> Args;
GenericValue value;
value.PointerVal = block;
Args.push_back(value);
engine->runFunction(function, Args);
}
下面是处理__block变量的逻辑,处理方法和copy/dispose类似。根据OC block的ABI,需要检测byref->flags&1<<25来判断是否需要处理byref_keep和byref_dispose。byref_keep可以复用copy_helper的实现,同理byref_dispose可以复用dispose_helper的实现。
ExecutionContext &SF = ECStack.back();
GenericValue src = getOperandValue(I.getOperand(0), SF);
struct Block_byref *byref = (struct Block_byref *)src.PointerVal;
if (byref->flags & 1<<25){
char *copyName = (char *)CFDictionaryGetValue(copyDic, byref);
if (copyName == nullptr){
Value *keep = (Value *)byref->byref_keep;
Value *dispose = (Value *)byref->byref_dispose;
CFDictionaryAddValue(copyDic, byref, keep->getName().data());
CFDictionaryAddValue(disposeDic, byref, dispose->getName().data());
byref->byref_keep = copy_helper;
byref->byref_dispose = dispose_helper;
}
}
此外还需要考虑直接调用一个block的情况,比如:
void(^testArgument)(int) = ^(int input){
printf("testArgument called with input:%d\n", input);
};
testArgument(456);
这个时候调用会被转换为IR内部的一个Call指令。所以需要在Call指令的实现内做一个检测,如果当前block的invoke函数已经被上述的操作替换了,那么需要将其替换回原本对应的LLVM Function以供直接调用。
Function *f = (Function *)CFDictionaryGetValue(functionDic, GVTOP(SRC));
if (f){
callFunction(f, ArgVals);
return;
}
2.5 ARC
还需要考虑arc的支持。在生成IR文件的过程中,使用-fobjc-arc会生成类似于llvm.objc.retainAutoreleasedReturnValue这种调用,然而这并不是实际的函数名称,实际的函数名应该是objc_retainAutoreleasedReturnValue这种形式。所以在解释执行IR的时机,我们要对这些调用做一个替换处理,arc相关的这些函数是一个有限集,逐个判断替换即可。
if (F->getName() == StringRef("llvm.objc.retain")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_retain"));
} else if (F->getName() == StringRef("llvm.objc.release")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_release"));
} else if (F->getName() == StringRef("llvm.objc.retainAutoreleasedReturnValue")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_retainAutoreleasedReturnValue"));
} else if (F->getName() == StringRef("llvm.objc.retainAutorelease")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_retainAutorelease"));
} else if (F->getName() == StringRef("llvm.objc.retainAutoreleaseRetureValue")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_retainAutoreleaseRetureValue"));
} else if (F->getName() == StringRef("llvm.objc.autorelease")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_autorelease"));
} else if (F->getName() == StringRef("llvm.objc.storeStrong")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_storeStrong"));
} else if (F->getName() == StringRef("llvm.objc.storeWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_storeWeak"));
} else if (F->getName() == StringRef("llvm.objc.copyWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_copyWeak"));
} else if (F->getName() == StringRef("llvm.objc.initWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_initWeak"));
} else if (F->getName() == StringRef("llvm.objc.loadWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_loadWeak"));
} else if (F->getName() == StringRef("llvm.objc.loadWeakRetained")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_loadWeakRetained"));
} else if (F->getName() == StringRef("llvm.objc.destroyWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_destroyWeak"));
} else if (F->getName() == StringRef("llvm.objc.moveWeak")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_moveWeak"));
} else if (F->getName() == StringRef("llvm.objc.retainBlock")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_retainBlock"));
} else if (F->getName() == StringRef("llvm.objc.autoreleaseReturnValue")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_autoreleaseReturnValue"));
} else if (F->getName() == StringRef("llvm.objc.autoreleasePoolPop")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_autoreleasePoolPop"));
} else if (F->getName() == StringRef("llvm.objc.autoreleasePoolPush")){
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(StringRef("objc_autoreleasePoolPush"));
} else {
RawFn = (RawFunc)(intptr_t)sys::DynamicLibrary::SearchForAddressOfSymbol(F->getName());
}
2.6 多线程
还需要考虑多线程的支持。日常的开发中dispatch_sync/dispatch_async类的函数会使用很多,涉及到block的处理和多线程的处理两部分。block的处理上面已经简单介绍过,多线程的方案目前处理的比较简单,为了避免线程安全的问题,每次需要在异步线程执行IR代码时,都会创建新的LLVM Context,Mudule,和ExecutionEngine来处理。但坏处在于性能会比较差,以及会有内存泄漏等问题,还有很大的优化空间。内存泄漏的问题也主要来自于此,比如像一些NSConcreteGlobalBlock类型的block对象是LLVM创建并保存的,这些block也可能会当作参数传递给系统的API,并且这些block对象的copy等操作都是空逻辑。所以如果对应的LLVM对象的内存被释放掉的话,等外部的方法再调用这些block的时候,就会出现崩溃。
后续可以针对性的做一些改进。一个比较理想的方案是给每个用到的线程,创建一个解释器并保存一份LLVM相关的对象,在某个线程需要执行代码的时候,获取到其对应的解释器和对象进行处理。
此外,如果解释器在执行某个函数的时候,调用到了我们自定义的函数,并且这个自定义的函数也需要解释器去执行另外的函数,那么也会出现一个问题,就是会破坏掉解释器现有的栈结构,这个时候也是需要创建一个新的解释器来执行另外的函数,避免对原有解释器的栈产生影响。
2.7 Struct
此外还有类似于CGRect这种数据结构的处理。比如CGRectMake这样的函数,其实际上是一个内联函数。但由于x86和arm64 ABI的差异,针对不同架构生成的IR也有不同的处理。由于现有的LLVM解释器的实现不完整,无法支持直接load一个struct类型的变量到其寄存器中,所以无法完全支持arm64架构下IR的解释执行。目前暂时没有发现x86和arm64下的IR有实际影响逻辑的差异性,所以可以暂时使用针对x86架构生成的IR文件来处理真机环境下的执行。
4 后续
朱雀的实现目前并不完整,还停留在原型阶段。等进一步完善后会开放源码以供大家共同探讨。
下面的工作计划大概如下:
a. 支持添加或者删除类的property
目前并不支持修改AClass的属性。比如AClass有两个property,view1和view2,我们添加了一个新的property,UIView类型,名字为view3。那么通常情况下,编译器会自动帮我们生成getter和setter,这两个方法也同样会存在于IR中。但是,当代码中真正使用view3时,这个getter方法是没办法正常运行的,原因在于,外部创建出来的AClass的对象AObject实际的内存空间其实并不包含view3,只有view1和view2。而getter会假定AObject的内存空间中是含有view3的,那么直接执行getter方法就会产生内存问题。众所周知OC的runtime可以支持给一个类新增一个方法,但并不会支持新增property,基本也是同样的原因。
实际上这个问题也是有办法在一定程度上支持的。需要在IR文件中获取到当前类的所有property,然后从runtime中获取到现有的property,对于那些新增的部分,在需要调用其getter和setter的时候,并不执行IR中的Function,而是使用自定义的实现,在自定义的实现中可以通过associated object等手段去处理。
b. 支持同时加载处理多个IR文件
目前只支持单个IR文件的加载,支持多个IR文件会允许解释执行更多的逻辑,甚至可以执行一个比较完整的代码模块。
c. 支持新增一个新的OC类
目前只支持runtime中已有类的IR文件解释执行,并不能通过处理一个新类的IR文件,向runtime中注册一个新的类。这会是一个比较复杂的工作。
5 闲谈
理论上来说,是可以做到基本的解释执行一个iOS App的所有代码,从AppDelegate开始,就完全进入到我们的世界了。当然像+load,kvc/kvo,以及各种hook等等可能并不能完全支持或者有很大难度。不过到了这个程度,壳App就会越来越像一个浏览器,而OC代码则等同于JS,绕了一圈又回到了所谓大前端的概念上。
在我们这儿的大环境下,单一原生客户端的开发感觉会越来越被忽视,一些跨端的,有一些动态化能力的解决方案可能更受欢迎。虽然是一个老生常谈的话题,但个人感觉跨端的开发能力确实应该重点关注,在很大程度上会缩减多端开发的时间和成本。所以趁着还算年轻,抓紧进入到Flutter和Dart的世界,这更像是移动开发的未来。