手机移动程序开发

朱雀-基于LLVM的Objective-C解释器的基本实现

2020-01-21  本文已影响0人  08a2d4bd26c9

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(&param, 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的世界,这更像是移动开发的未来。

上一篇下一篇

猜你喜欢

热点阅读