一、Calling Convention
高级语言编译器将代码编译成相应汇编指令时都会依据一系列的规则,这些规则十分必要,特别是对编译来说。其中之一是“调用约定” (Calling Convention),它包含了编译器关于函数入口处的函数参数、函数返回值的一系列假设。它有时也被称作“ABI”(Application Binary Interface)。调用约定(Calling Conventions)定义了程序中调用函数的方式,它决定了在函数调用的时候数据(比如说参数)在堆栈中的组织方式。
编译器按照调用规则去编译,把数据放到相应的堆栈中,那么意味着函数的调用方和被调用方(函数本身)也要遵循这个统一的约定,不然函数执行过程中,会因为取不到相应类型参数和无法正确返回而崩溃。
下面看个例子:
int testFunc(int n, int m) {
return n+m;
}
int main() {
int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2);
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2);
return 0;
}
运行上面代码发现,(1)处正常执行,(2)处崩溃,原因就是因为(2)处funcPointer的定义没有遵循调用规则,和原函数本身的定义不符,而编译器编译的时候认为funcPointer是个无参数函数,则不会在执行栈上分配两个int型的内存空间用来存储实参1,2;这样当exp指针跳转到代码区执行原函数,去取对应参数时,会出现取不到或取到的内容有误的情况,从而导致崩溃。
可见我们在函数调用前,需要明确的告诉编译器这个函数的参数和返回值类型是什么,函数才能正常执行。
那这样来说动态的调用一个C函数是不可能实现了,因为我们在编译前,就要将遵循调用规则的函数调用写在需要调用的地方,然后通过编译器编译生成对应的汇编代码,将相应的栈和寄存器状态准备好。如果想在运行时动态去调用的话,将没有人为我们做这一系列的处理。
所以我们要解决的问题是:当我们在运行时动态调用一个函数时,自己要先将相应栈和寄存器状态准备好,然后生成相应的汇编指令。这也正是libffi所做的。
二、libffi
FFI(Foreign Function Interface)允许以一种语言编写的代码调用另一种语言的代码,而libffi库提供了最底层的、与架构相关的、完整的FFI。libffi的作用就相当于编译器,它为多种调用规则提供了一系列高级语言编程接口,然后通过相应接口完成函数调用,底层会根据对应的规则,完成数据准备,生成相应的汇编指令代码。
那么这样我们就可以通过libffi动态的调用任意C函数,那libffi 具体怎么使用呢?详细文档请看:这里
三、动态调用C函数
使用libffi提供接口动态调用流程如下:
需使用的libffi API:
ffi_status ffi_prep_cif(ffi_cif *cif,
ffi_abi abi,
unsigned int nargs,
ffi_type *rtype,
ffi_type **atypes);
void ffi_call(ffi_cif *cif,
void (*fn)(void),
void *rvalue,
void **avalue);
下面看一个简单的例子:
int testFunc(int m, int n) {
printf("params: %d %d \n", m, n);
return m+n;
}
+ (void)testCall {
testFunc(1, 2);
void* functionPtr = &testFunc;
int argCount = 2;
ffi_type **ffiArgTypes = alloca(sizeof(ffi_type *) *argCount);
ffiArgTypes[0] = &ffi_type_sint;
ffiArgTypes[1] = &ffi_type_sint;
void **ffiArgs = alloca(sizeof(void *) *argCount);
void *ffiArgPtr = alloca(ffiArgTypes[0]->size);
int *argPtr = ffiArgPtr;
*argPtr = 5;
ffiArgs[0] = ffiArgPtr;
void *ffiArgPtr2 = alloca(ffiArgTypes[1]->size);
int *argPtr2 = ffiArgPtr2;
*argPtr2 = 3;
ffiArgs[1] = ffiArgPtr2;
ffi_cif cif;
ffi_type *returnFfiType = &ffi_type_sint;
ffi_status ffiPrepStatus = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (unsigned int)argCount, returnFfiType, ffiArgTypes);
if (ffiPrepStatus == FFI_OK) {
void *returnPtr = NULL;
if (returnFfiType->size) {
returnPtr = alloca(returnFfiType->size);
}
ffi_call(&cif, functionPtr, returnPtr, ffiArgs);
int returnValue = *(int *)returnPtr;
printf("ret: %d \n", returnValue);
}
}
执行结果:
params: 1 2
params: 5 3
ret: 8
可见使用ffi,只要有函数原型cif对象,函数实现指针,返回值内存指针和函数参数数组,我们就可以实现在运行时动态调用任意C函数。
所以如果想实现其他语言(譬如JS),执行过程中动态调用C函数,只需在调用过程中加一层转换,将参数及返回值类型转换成libffi对应类型,并封装成函数原型cif对象,准备好参数数据,找到对应函数指针,然后调用即可。
四、动态定义C函数
libffi还有一个特别强大的函数,通过它我们可以将任意参数和返回值类型的函数指针,绑定到一个函数实体上。那么这样我们就可以很方便的实现动态定义一个C函数了!同时这个函数在编写解释器或提供任意函数的包装器(通用block)时非常有用,此函数是:
ffi_status ffi_prep_closure_loc (ffi_closure *closure,
ffi_cif *cif,
void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data),
void *user_data,
void *codeloc)
来看下函数各参数详细说明:
Prepare a closure function.
参数 closure is the address of a ffi_closure object; this is the writable address returned by ffi_closure_alloc.
参数 cif is the ffi_cif describing the function parameters.
参数 user_data is an arbitrary datum that is passed, uninterpreted, to your closure function.
参数 codeloc is the executable address returned by ffi_closure_alloc.
函数实体 fun is the function which will be called when the closure is invoked. It is called with the arguments:
函数实体参数 cif
The ffi_cif passed to ffi_prep_closure_loc.
函数实体参数 ret
A pointer to the memory used for the function's return value. fun must fill this, unless the function is declared as returning void.
函数实体参数 args
A vector of pointers to memory holding the arguments to the function.
函数实体参数 user_data
The same user_data that was passed to ffi_prep_closure_loc.
ffi_prep_closure_loc will return FFI_OK if everything went ok, and something else on error.
After calling ffi_prep_closure_loc, you can cast codeloc to the appropriate pointer-to-function type.
You may see old code referring to ffi_prep_closure. This function is deprecated, as it cannot handle the need for separate writable and executable addresses.
下面通过一个简单的例子,看下如何将一个函数指针绑定到一个函数实体上:
#include <stdio.h>
#include <ffi.h>
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;
closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);
if (closure)
{
args[0] = &ffi_type_pointer;
if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1,
&ffi_type_uint, args) == FFI_OK)
{
if (ffi_prep_closure_loc(closure, &cif, puts_binding,
stdout, bound_puts) == FFI_OK)
{
rc = bound_puts("Hello World!");
}
}
}
ffi_closure_free(closure);
return 0;
}
上述步骤大致分为:
- 准备一个函数实体
- 声明一个函数指针
- 根据函数参数个数/参数及返回值类型生成一个函数原型
- 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来
- 释放closure
通过以上这5步,我们就可以在执行过程中将一个函数指针,绑定到一个函数实体上,从而轻而易举的实现动态定义一个C函数。
由上可知:如果我们利用好user_data,用其传入我们想要的函数实现,将函数实体变成一个通用的函数实体,然后将函数指针改为void*,通过结构体创建一个block保存函数指针并返回,那么我们就可以实现JS调用含有任意类型block参数的OC方法了(后续文章会简要概述说明)
到这我们已经清楚的了解了libffi的秒用,以后实际应用中,我们可以利用它轻松实现多种语言之间的互相调用。
转自: