这段时间在做一个组件开发,要实现JS那边动态调用一个含有block参数的OC方法,接触到了libffi,主要涉及使用libffi 动态调用和定义C函数两个方面,下面是使用之后的一些总结。参考了bang的博客这里
一、Calling Convention
高级语言编译器将代码编译成相应汇编指令时都会依据一系列的规则,这些规则十分必要,特别是对独立编译来说。其中之一是“调用约定” (Calling Convention),它包含了编译器关于函数入口处的函数参数、函数返回值的一系列假设。它有时也被称作“ABI”(Application Binary Interface)。调用约定(Calling Conventions)定义了程序中调用函数的方式,它决定了在函数调用的时候数据(比如说参数)在堆栈中的组织方式。
编译器按照调用规则去编译,把数据放到相应的堆栈中,那么意味着函数的调用方和被调用方(函数本身)也要遵循这个统一的约定,不然函数执行过程中,会因为取不到相应类型参数和无法正确返回而崩溃。
下面看个例子:
int testFunc(int n, int m) {
return n+m;
}
int main() {
// (1)
int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2);
// (2)
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2); //error
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提供接口动态调用流程如下:
- 准备好参数数据及其对应ffi_type数组、返回值内存指针、函数指针
- 创建与函数特征相匹配的函数原型:
ffi_cif
对象 - 使用“ffi_call”来完成函数调用
需使用的libffi API:
/* 封装函数原型
ffi_prep_cif returns a libffi status code, of type ffi_status. This will be either FFI_OK if everything worked properly; FFI_BAD_TYPEDEF if one of the ffi_type objects is incorrect; or FFI_BAD_ABI if the abi parameter is invalid.
*/
ffi_status ffi_prep_cif(ffi_cif *cif,
ffi_abi abi, //abi is the ABI to use; normally FFI_DEFAULT_ABI is what you want. Multiple ABIs for more information.
unsigned int nargs, //nargs is the number of arguments that this function accepts. ‘libffi’ does not yet handle varargs functions; see Missing Features for more information.
ffi_type *rtype, //rtype is a pointer to an ffi_type structure that describes the return type of the function. See Types.
ffi_type **atypes); //argtypes is a vector of ffi_type pointers. argtypes must have nargs elements. If nargs is 0, this argument is ignored.
/* 调用指定函数
This calls the function fn according to the description given in cif. cif must have already been prepared using ffi_prep_cif.
*/
void ffi_call(ffi_cif *cif,
void (*fn)(void),
void *rvalue, //rvalue is a pointer to a chunk of memory that will hold the result of the function call. This must be large enough to hold the result and must be suitably aligned; it is the caller's responsibility to ensure this. If cif declares that the function returns void (using ffi_type_void), then rvalue is ignored. If rvalue is ‘NULL’, then the return value is discarded.
void **avalue); //avalues is a vector of void * pointers that point to the memory locations holding the argument values for a call. If cif declares that the function has no arguments (i.e., nargs was 0), then avalues is ignored. Note that argument values may be modified by the callee (for instance, structs passed by value); the burden of copying pass-by-value arguments is placed on the caller.
下面看一个简单的例子:
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_cfi 对象
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);
}
//根据cif函数原型,函数指针,返回值内存指针,函数参数数据调用这个函数
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_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>
/* 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)
{
/* 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 */
// 通过 ffi_closure 把 函数原型_cifPtr / 函数实体JPBlockInterpreter / 上下文对象self / 函数指针blockImp 关联起来
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;
}
上述步骤大致分为:
- 准备一个函数实体
- 声明一个函数指针
- 根据函数参数个数/参数及返回值类型生成一个函数原型
- 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来
- 释放closure
通过以上这5步,我们就可以在执行过程中将一个函数指针,绑定到一个函数实体上,从而轻而易举的实现动态定义一个C函数。
由上可知:如果我们利用好user_data,用其传入我们想要的函数实现,将函数实体变成一个通用的函数实体,然后将函数指针改为void,通过结构体创建一个block保存函数指针并返回,那么我们就可以实现JS调用含有任意类型block参数的OC方法了(后续文章会简要概述说明)*
到这我们已经清楚的了解了libffi的秒用,以后实际应用中,我们可以利用它轻松实现多种语言之间的互相调用。