首页 如何动态调用 C 函数
文章
取消

如何动态调用 C 函数

动态语言和静态语言的区别

动态编程语言是高级编程语言的一个类别,在计算机科学领域已被广泛应用。它是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。

在编译时,变量的数据类型就可以确定的语言,大多数静态语言要求在使用变量之前必须声明数据类型。

如何实现 C 语言的动态调用

正常情况下的调用:

1
2
3
4
5
6
7
8
9
//main.m
void test() {

}
  
int main() {
    test()
    return 0;
}

高级点的:

1
2
3
4
5
6
7
8
9
10
11
//main.m
void test() {

}
  
int main() {
    void (*funcPointer)();
    funcPointer = test;
    funcPointer();
    return 0;
}

若函数对外部是不可引用状态下,上述方法都无法实现对 C 函数的调用。那么我们能否像 OC Runtime 一样实现 C 函数的动态调用呢?例如,如何动态调用一个未在 Public Header 声明的 C 函数呢?

1
2
3
4
5
6
#ifndef TEST_FUNCTION_H
#define TEST_FUNCTION_H
  
extern void fun1();
  
#endif //TEST_FUNCTION_H
1
2
3
4
5
6
7
#ifndef TEST_INTERNAL_H
#define TEST_INTERNAL_H
  
void doSomething();
void doSomething2();
  
#endif //TEST_INTERNAL_H
1
2
3
4
5
6
#include "Function.h"
#include "Internal.h"
  
void fun1() {
    doSomething();
}

函数地址

首先若要动态调用 C 函数,最根本的问题就是要上述找到函数地址。然后我们知道一般代码编译完成后生产的文件是一个带符号描述二进制表示。我们先来看一下 libFunctionLib.dylib 动态库的实际内容是什么:

从图中我们能看到二进制内部有一段和函数名非常接近的字符串表示,那么我们能不能通过这个字符串找到对应的函数地址呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <dlfcn.h>
  
int main() {
    ...
  
    /* open the needed object */
    void *handle;
    handle = dlopen("/Users/JaxWu/CLionProjects/Test/cmake-build-debug/FunctionLib/libFunctionLib.dylib", RTLD_NOW);
  
    /* find address of function and data objects */
    void (*funcPointer2)() = dlsym(handle, "doSomething2");
    funcPointer2();
  
    dlclose(handle);
  
    ...
    return 0;
}

现在我们解决了简单 C 语言的函数动态调用,那么对于更普遍的带参函数我们又该如何进行动态调用呢?例如,有一个加法操作的 C 函数:

1
2
3
4
int addFunc(int m, int n) {
    printf("Result %d", m + n );
    return m + n;
}

Calling Convention

一个函数的调用过程中,函数的参数既可以使用栈传递,也可以使用寄存器传递,参数压栈的顺序可以从左到右也可以从右到左,函数调用后参数从栈弹出这个工作可以由函数调用方完成,也可以由被调用方完成。如果函数的调用方和被调用方(函数本身)不遵循统一的约定,有这么多分歧这个函数调用就没法完成。这个双方必须遵守的统一约定就叫做调用惯例(Calling Convention),调用惯例规定了参数的传递的顺序和方式,以及栈的维护方式。

所以你需要在调用前明确告诉编译器这个函数的参数和返回值类型是什么,编译器才能生成对应的正确的汇编代码,让被调用的函数执行时能正常取到参数。也就是说如果需要动态调用任意 C 函数,就得先准备好任意 参数类型/参数个数/返回值类型 排列组合的 C 函数指针,让最终的汇编把所有情况都准备好,最后调用时通过 switch 去找到正确的那个去执行就可以了。

objc_msgSend

实际上你会发现 OC 上有个函数脱离了上述限制,就是 objc_msgSend。OC 所有方法调用最终都会走到 objc_msgSend去调用,这个神奇的方法支持任意返回值任意参数类型和个数,而它的定义仅是这样:

1
void objc_msgSend(void /* id self, SEL op, ... */ )

为什么它就可以支持所有函数调用呢?

答案是在C语言层面上没区别,但人家在汇编上做了手脚,objc_msgSend 是用汇编写的,在调用这个函数之前,会把栈/寄存器等数据都准备好,相当于调用前对参数入栈等处理由这个函数自己写的汇编代码接管了,不需要编译器在调用处去生成这些指令。

这里会在调用真正的函数之前,根据 Calling Convention 准备好栈帧/寄存器数据和状态,最后再 jump/call 到函数实体执行就可以了,这时函数实体按约定去取参数是取得到的,可以正常执行。于是 objc 就做到了在编译前只需要定义一个简单的 objc_msgSend,就支持运行时动态调用任意类型的 C 函数(所有 OC 方法的 IMP)。

所以我们要仿照 objc_msgSend做一遍这个事情吗?难度好高:(。不用怕, libffi 这个神器已经帮你做了。

libffi

对 libffi 的介绍可以看 地址,简单来说它就是提供了动态调用任意 C 函数的功能。

先来看看怎样通过 libffi 动态调用一个 C 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main() {
    //拿函数指针
    void* functionPtr = dlsym(RTLD_DEFAULT, "testFunc");
    int argCount = 2;
  
    //按ffi要求组装好参数类型数组
    ffi_type **ffiArgTypes = alloca(sizeof(ffi_type *) *argCount);
    ffiArgTypes[0] = &ffi_type_sint;
    ffiArgTypes[1] = &ffi_type_sint;
  
    //按ffi要求组装好参数数据数组
    void **ffiArgs = alloca(sizeof(void *) *argCount);
    void *ffiArgPtr = alloca(ffiArgTypes[0]->size);
    int *argPtr = ffiArgPtr;
    *argPtr = 1;
    ffiArgs[0] = ffiArgPtr;
  
    void *ffiArgPtr2 = alloca(ffiArgTypes[1]->size);
    int *argPtr2 = ffiArgPtr2;
    *argPtr2 = 2;
    ffiArgs[1] = ffiArgPtr2;
  
    //生成 ffi_cfi 对象,保存函数参数个数/类型等信息,相当于一个函数原型
    ffi_cif cif;
    ffi_type *returnFfiType = &ffi_type_sint;
    ffi_status ffiPrepStatus = ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, (unsigned int)0, (unsigned int)argCount, returnFfiType, ffiArgTypes);
  
    if (ffiPrepStatus == FFI_OK) {
        //生成用于保存返回值的内存
        void *returnPtr = NULL;
        if (returnFfiType->size) {
            returnPtr = alloca(returnFfiType->size);
        }
        //根据cif函数原型,函数指针,返回值内存指针,函数参数数据调用这个函数
        ffi_call(&cif, functionPtr, returnPtr, ffiArgs);
  
        //拿到返回值
        int returnValue = *(int *)returnPtr;
        printf("ret: %d ", returnValue);
    }
}

动态定义 C 函数

libffi还有一个特别强大的函数,通过它我们可以将任意参数和返回值类型的函数指针,绑定到一个函数实体上。那么这样我们就可以很方便的实现动态定义一个C函数了!同时这个函数在编写解释器或提供任意函数的包装器(通用block)时非常有用,此函数是:

1
2
3
4
5
ffi_status ffi_prep_closure_loc (ffi_closure *closure, //闭包,一个ffi_closure对象
ffi_cif *cif, //函数原型
void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data), //函数实体
void *user_data, //函数上下文,函数实体实参
void *codeloc) //函数指针,指向函数实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <ffi.h>
  
/* Acts like puts with the file given at time of enclosure. */
// 函数实体
void puts_binding(ffi_cif *cif, unsigned int *ret, void* args[],
FILE *stream) {
    *ret = fputs(*(char **)args[0], stream);
}
  
int main() {
    ffi_cif cif;
    ffi_type *args[1];
    ffi_closure *closure;
    int (*bound_puts)(char *); //声明一个函数指针
    int rc;
    /* Allocate closure and bound_puts */ //创建closure
    closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);
    if (!closure) {
        return  0;
    }

     /* Initialize the argument info vectors */
    args[0] = &ffi_type_pointer;
    /* Initialize the cif */ //生成函数原型
    if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &ffi_type_uint, args) == FFI_OK) {
        /* Initialize the closure, setting stream to stdout */
        if (ffi_prep_closure_loc(closure, &cif, puts_binding, 
            stdout, bound_puts) == FFI_OK) {
            
            rc = bound_puts("Hello World!");
            /* rc now holds the result of the call to fputs */
        }
    }
        /* Deallocate both closure, and bound_puts */
    ffi_closure_free(closure); //释放闭包
    
    return 0;
}

上述步骤大致分为:

  1. 准备一个函数实体
  2. 声明一个函数指针
  3. 根据函数参数个数/参数及返回值类型生成一个函数原型
  4. 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来
  5. 释放closure

通过以上这5步,我们就可以在执行过程中将一个函数指针,绑定到一个函数实体上,从而轻而易举的实现动态定义一个C函数。

由上可知:如果我们利用好user_data,用其传入我们想要的函数实现,将函数实体变成一个通用的函数实体,然后将函数指针改为 void,通过结构体创建一个 block 保存函数指针并返回,那么我们就可以实现JS 调用含有任意类型 block 参数的 OC 方法了,或者实现另一种 AOP 编程

本文由作者按照 CC BY 4.0 进行授权

剖析 ARM 64 架构中的 objc_msgSend

-