前言:
对于C语言来说,我们在函数调用前,需要明确的告诉编译器这个函数的参数和返回值类型是什么,函数才能正常执行。
如此说来动态的调用一个C函数是不可能实现的。
因为我们在编译前,就要将遵循调用规则的函数调用写在需要调用的地方,然后通过编译器编译生成对应的汇编代码,将相应的栈和寄存器状态准备好。
如果想在运行时动态去调用的话,将没有人为我们做这一系列的处理。
所以我们需要解决这个问题:当我们在运行时动态调用一个函数时,自己要先将相应栈和寄存器状态准备好,然后生成相应的汇编指令。而 <mark>libffi</mark> 库正好替我们解决了此难题。
什么是FFI?
FFI(Foreign Function Interface)允许以一种语言编写的代码调用另一种语言的代码,而libffi库提供了最底层的、与架构相关的、完整的FFI。libffi的作用就相当于编译器,它为多种调用规则提供了一系列高级语言编程接口,然后通过相应接口完成函数调用,底层会根据对应的规则,完成数据准备,生成相应的汇编指令代码。
FFI 的核心API
(1)函数原型结构体
typedef struct {
ffi_abi abi;
unsigned nargs;
ffi_type **arg_types;
ffi_type *rtype;
unsigned bytes;
unsigned flags;
#ifdef FFI_EXTRA_CIF_FIELDS
FFI_EXTRA_CIF_FIELDS;
#endif
} ffi_cif;
(2)封装函数原型
/* 封装函数原型
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.
(3)函数对象的回调结构体
typedef struct {
#if 0
void *trampoline_table;
void *trampoline_table_entry;
#else
char tramp[FFI_TRAMPOLINE_SIZE];
#endif
ffi_cif *cif;
void (*fun)(ffi_cif*,void*,void**,void*);
void *user_data;
} ffi_closure
(4)函数回调对象的创建
int (* blockImp)(char *); //声明一个函数指针
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);
(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) //函数指针,指向函数实体
将函数的参数等数据信息与回调对象_closure关联起来,当程序调用到此函数时,也会执行此回调地址的代码,同时将获得此函数的所有参数
调用:ffi_prep_closure_loc(closure, &cif, calCircleArea,stdout, bound_calCircleArea)
参数解析:
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.
(6)释放函数回调
ffi_closure_free(closure); //释放闭包
如何动态调用 C 函数
(1)步骤:
1. 准备一个函数实体
2. 声明一个函数指针
3. 根据函数参数个数/参数及返回值类型生成一个函数原型
4. 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来
5. 释放closure
(2)示例代码:
#include <stdio.h>
#include "libffi/ffi.h"
// 函数实体
void calCircleArea(ffi_cif *cif,
float *ret,
void *args[],
FILE *stream) {
float pi = 3.14;
float r = **(float **)args[0];
float area = pi * r * r;
*ret = area;
printf("我是那个要被动态调用的函数\n area:%.2f\n *ret = %.2f",area,*ret);
}
int main(int argc, const char * argv[]) {
///函数原型
ffi_cif cif;
///参数
ffi_type *args[1];
///回调闭包
ffi_closure *closure;
///声明一个函数指针,通过此指针动态调用已准备好的函数
float (*bound_calCircleArea)(float *);
float rc = 0;
/* Allocate closure and bound_calCircleArea */ //创建closure
closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_calCircleArea);
if (closure) {
/* Initialize the argument info vectors */
args[0] = &ffi_type_pointer;
/* Initialize the cif */ //生成函数原型 &ffi_type_float:返回值类型
if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &ffi_type_float, args) == FFI_OK) {
/* Initialize the closure, setting stream to stdout */
// 通过 ffi_closure 把 函数原型_cifPtr / 函数实体JPBlockInterpreter / 上下文对象self / 函数指针blockImp 关联起来
if (ffi_prep_closure_loc(closure, &cif, calCircleArea,stdout, bound_calCircleArea) == FFI_OK) {
float r = 10.0;
//当执行了bound_calCircleArea函数时,获得所有输入参数, 后续将执行calCircleArea。
//动态调用calCircleArea
rc = bound_calCircleArea(&r);
printf("rc = %.2f\n",rc);
}
}
}
/* Deallocate both closure, and bound_calCircleArea */
ffi_closure_free(closure); //释放闭包
return 0;
}
由上可知:如果我们利用好ffi_prep_closure_loc 的第四个参数 user_data,用其传入我们想要的函数实现,将函数实体变成一个通用的函数实体,然后将函数指针改为void*,通过结构体创建一个block保存函数指针并返回,那么我们就可以实现JS调用含有任意类型block参数的OC方法了。
总结
根据以上的思想:
我们可以将 ffi_closure 关联的指针替换原方法的IMP,
当对象收到该方法的消息时 objc_msgSend(id self, SEL sel, ...) ,
将最终执行自定义函数 void ffifunction(fficif *cif, void *ret, void **args, void *userdata) 。
而实现这一切的主要工作是:设计可行的结构,存储类的多个hook信息;
根据包含不同参数的方法和切面block,生成包含匹配 ffi_type 的cif;
替换类某个方法的实现为 ffi_closure 关联的imp,记录hook;
在 ffi_function 里,根据获得的参数,动态调用原始imp和block。
后续我们可以利用这个库,结合lex 及 yacc将OC或任意的语言编译成C代码去执行,从而实现hotfix。