上一篇文章中我们了解了函数指针,函数地址,调用惯例及libffi动态调用C函数的例子,最后也提出一个问题:如果动态定义C函数,支持不同类型及个数的参数呢
libffi同样支持在运行时动态定义C函数
先举个例子看下怎么使用的
void JPBlockInterpreter(ffi_cif *cif, void *ret, void **args, void *userdata)
{
//①
//函数实体
//通过 userdata / args 提取参数
//返回值赋给 ret
}
void main() {
//②
ffi_type *returnType = &ffi_type_void;
NSUInteger argumentCount = 2;
ffi_type **_args = malloc(sizeof(ffi_type *)*argumentCount) ;
_args[0] = &ffi_type_sint;
_args[1] = &ffi_type_pointer;
ffi_cif *_cifPtr = malloc(sizeof(ffi_cif));
ffi_prep_cif(_cifPtr, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args);
//③
void *blockImp = NULL;
//④
ffi_closure *_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&blockImp);
ffi_prep_closure_loc(_closure, _cifPtr, JPBlockInterpreter, (__bridge void *)self, blockImp);
}
流程:
- 准备一个通用的函数JPBlockInterpreter
- 根据函数参数个数,参数类型返回值类型封装cif对象,表示原型
- 准备一个函数指针(壳),用于调用
- 通过ffi_closure把实现函数原型_cifPtr / 函数实体JPBlockInterpreter / 上下文对象self / 函数指针blockImp 关联起来。
- 调用函数指针blockImp的时候,会调用JPBlockInterpreter。其中JPBlockInterpreter的userdata就是传入的上下文
上述例子中,这一系列处理后 blockImp 就可以当成一个指向返回值类型是void,参数类型是 (int a, NSString *b) 的函数去调用,调用后会去到 JPBlockInterpreter 这个函数实体,在这个函数里面可以通过 args 提取传进来的参数,通过userdata 取上下文进行处理。这里可以根据参数类型的不同动态创建不同的 cif 对象,生成不同的 blockImp 函数指针。
block
在之前一篇文章中聊过block,了解到block本质是个OC对象。此外,block是个匿名函数,block在编译后会生成相应的结构体和函数指针,这边就不详细阐述。简单的来讲,block是一个包含函数指针的结构体。
举个例子:
struct JPSimulateBlock {
void *isa;
int flags;
int reserved;
void *invoke;
struct JPSimulateBlockDescriptor *descriptor;
};
struct JPSimulateBlockDescriptor {
unsigned long int reserved;
unsigned long int size;
};
void blockImp(){
NSLog(@"call block succ");
}
void genBlock() {
struct JPSimulateBlockDescriptor descriptor = {0, sizeof(struct JPSimulateBlock)};
struct JPSimulateBlock simulateBlock = {
&_NSConcreteStackBlock,
0, 0, blockImp, &descriptor
};
void *blockPtr = &simulateBlock;
void (^blk)() = ((__bridge id)blockPtr);
blk(); //output "call block succ"
}
一个存有函数指针的特定结构体就是一个 block,调用这个 block 就是调用里面函数指针指向的函数
signature
这里需要补充下block的签名。block签名中包含了参数类型和个数相关信息,是构建cif模板的必要条件。
一个 block 的签名格式是:[返回值类型和偏移][@?0][参数0类型和偏移][参数1类型和偏移]…,比如 arm64 下 int (^block)(int, int) 的签名是 i16@?0i8i12。block 指针占 8 字节,参数和返回值 int 都是 4 字节。
然后需要把 signature 字符串处理分拆成参数类型列表,在 libffi 中使用 ffi_type 表示各种类型。
有了参数类型列表,返回值类型,参数个数后,就可以调用 ffi_prep_cif() 函数创建 ffi_cif 了
block 完整的数据结构如下
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30) // compiler
};
// revised new layout
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
unsigned long int reserved;
unsigned long int size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout;
};
struct Block_layout {
void *isa;
volatile int flags; // contains ref count
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
block 的 flags 有两个位 BLOCK_HAS_COPY_DISPOSE(1 << 25) / BLOCK_HAS_SIGNATURE(1 << 30) 分别表示这个 block 的 descriptor 有没有 Block_descriptor_2 和 Block_descriptor_3 这两组数据。block 的签名就保存在 Block_descriptor_3 结构体里。所以如果我们要让 block 有 signature 签名,就需要:block 的 flags 需要有 BLOCK_HAS_SIGNATURE 标记,表示这个 block 有 signature 数据。
然后根据 block 的 flag 位掩码计算偏移拿到 Type Encoding 签名 signature
杂谈
目前这2天都在看libffi相关的东西,比如如何动态调用C函数,如何动态定义C函数,如何hook block之类的。hook block
这块内容有点多,涉及到的细节也不少,等后面消化完了,有时间了记录下。
因为前段时间在备考(寻找下一家),很久没更新了,趁这段时间赶紧补一下。另外也趁这段时间沉淀下,静一静(最近这段时间面试有点躁)。等国庆回来再观望下有没有合适的机会吧
网友评论