译文 · 原文: Friday Q&A 2011-05-06: A Tour of MABlockClosure · 作者 Mike Ash
原文:https://www.mikeash.com/pyblog/friday-qa-2011-05-06-a-tour-of-mablockclosure.html 发布:2011-05-06 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样
迟到了一周,但最新一期的 Friday Q & A 终于来了。大约一年前,我曾撰写过关于在运行时构建代码来将 block(代码块)转换为函数指针的文章。那是一次有趣的尝试,但由于各种限制最终不太实用。在此期间,我编写了 MABlockClosure,这是一种更健壮、更实用的实现方式,但我从未发表过相关文章。Landon Fuller 建议我讨论一下它的工作原理,因此这就是我今天要讲述的内容。
回顾
Block 是一种极其有用的语言特性,原因有二:它们允许在其它代码中内联编写匿名函数,并且可以通过引用其所在作用域中的局部变量来捕获该作用域的上下文。除此之外,这使得回调模式(callback patterns)变得简单得多。
struct CallbackContext { NSString *title; int value; };
static void MyCallback(id result, void *contextVoid) { struct CallbackContext *context = contextVoid; // use result, context->title, and context->value }
struct CallbackContext ctx; ctx.title = [self title]; ctx.value = [self value]; CallAPIWithCallback(workToDo, MyCallback, &ctx;); CallAPIWithCallbackBlock(workToDo, ^(id result) { // use result, [self title], [self value] });问题在于,并非所有基于回调的 API 都有接受 block(块)的版本。MABlockClosure 和我早期实验性的 trampoline 代码所实现的功能,是将一个 block 转换为可传递给这类 API 的函数指针。例如,如果不存在CallAPIWithCallbackBlock,MABlockClosure 允许你编写出几乎同样优雅的代码:
CallAPIWithCallback(workToDo, BlockFptrAuto(^(id result) { // use result, [self title], [self value] }));Blocks 会编译成一个函数和几个结构体。函数包含代码,结构体则保存块的信息,包括捕获的上下文(captured context)。该函数包含一个隐式参数(implicit argument),与 Objective-C 方法的 self 参数类似,指向块结构体。上面的块会被翻译成类似这样的形式:
void BlockImpl(struct BlockStruct *block, id info) { // code goes here }我最初尝试用一小段汇编代码实现跳板函数(trampoline)。这段代码试图以通用方式移动参数,然后在开头插入指针。遗憾的是,对于所有情况根本无法用同一段代码实现,最终导致了许多恼人的限制。
在当时,这已经是能实现的最佳方案了。幸运的是,苹果后来为块(block)添加了类型元数据(type metadata)。只要使用的编译器足够新以生成此元数据(任何较新的 clang 都能满足),就能用它来生成能进行适当参数操作的智能跳板代码。
libffi(外部函数接口)尽管块类型元数据提供了执行必要参数转换所需的全部信息,但这仍然是一项极其复杂的任务。具体需要执行的操作很大程度上取决于代码运行所在特定架构的函数调用 ABI(应用程序二进制接口),以及涉及的具体参数类型。
如果这一切都需要我亲自来实现,那么我将永远无法投入如此巨大的精力。好消息是,已经有一个现成的库知道如何为大量不同的架构处理所有这些:libffi。
libffi 提供了两大主要功能。它最著名的能力是能够以任意参数调用任意函数,而这些参数的类型直到运行时(runtime)才为人所知。一个较少被提及的功能则提供了本质上相反的作用:它允许创建「闭包(closure)」,即运行时生成的函数,这些函数能捕获任意参数,而参数类型同样是直到运行时才确定。
后者正是我们生成 block 的蹦床函数(trampoline function)所需要的。它以一种能够被 C 代码操纵的形式来捕获参数。然后,该代码可以根据需要操纵这些参数,并使用前一个功能来调用 block 的实现指针。
Support Structures(支持结构体)
block 结构体的布局并未在任何公开的头文件中披露。然而,由于这些结构体在编译时会被固化到可执行文件中,我们可以安全地从规范中提取它们,并依赖于与之匹配的实现。
以下是相关结构体:
struct BlockDescriptor { unsigned long reserved; unsigned long size; void *rest[1]; };
struct Block { void *isa; int flags; int reserved; void *invoke; struct BlockDescriptor *descriptor; }; static void *BlockImpl(id block) { return ((struct Block *)block)->invoke; } static const char *BlockSig(id blockObj) { struct Block *block = (void *)blockObj; struct BlockDescriptor *descriptor = block->descriptor;
int copyDisposeFlag = 1 << 25; int signatureFlag = 1 << 30;
assert(block->flags & signatureFlag);
int index = 0; if(block->flags & copyDisposeFlag) index += 2;
return descriptor->rest[index]; }很多必要的 libffi 数据结构需要根据类型签名动态创建。手动管理这些内存会很繁琐。既然这些数据结构的生命周期与闭包对象本身绑定,最简单的处理方式就是在对象内部追踪分配记录。为此我使用了一个 NSMutableArray。当需要分配内存时,我会创建合适大小的 NSMutableData,将其加入这个数组,然后返回它的 mutableBytes 指针。这个数组是该类的第一个实例变量:
@interface MABlockClosure : NSObject { NSMutableArray *_allocations; ffi_cif _closureCIF; ffi_cif _innerCIF; int _closureArgCount; ffi_closure *_closure; void *_closureFptr; id _block; } - (id)initWithBlock: (id)block;
- (void *)fptr;
@end - (void *)fptr { return _closureFptr; } - (id)initWithBlock: (id)block { if((self = [self init])) { _allocations = [[NSMutableArray alloc] init]; _block = block; _closure = AllocateClosure(&_closureFptr); [self _prepClosureCIF]; [self _prepInnerCIF]; [self _prepClosure]; } return self; }新版本的 libffi 将所有这些操作封装在 allocate(分配)、prepare(准备)和 deallocate(释放)闭包的调用中。如果你从源码构建 libffi,你会获得这种封装方式,在 iOS 上也是如此。MABlockClosure 被设计为能同时处理这两种方式。
AllocateClosure 函数使用条件编译来决定采用哪种技术。如果设置了 USE_LIBFFI_CLOSURE_ALLOC,它就直接调用 libffi 的相应函数。否则,它使用 mmap 来分配内存,这可以确保内存对齐正确,并且之后可以被标记为可执行。该函数如下所示:
static void *AllocateClosure(void **codePtr) { #if USE_LIBFFI_CLOSURE_ALLOC return ffi_closure_alloc(sizeof(ffi_closure), codePtr); #else ffi_closure *closure = mmap(NULL, sizeof(ffi_closure), PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if(closure == (void *)-1) { perror("mmap"); return NULL; } *codePtr = closure; return closure; #endif } static void DeallocateClosure(void *closure) { #if USE_LIBFFI_CLOSURE_ALLOC ffi_closure_free(closure); #else munmap(closure, sizeof(ffi_closure)); #endif }由 -initWithBlock: 调用的两个预备方法(prep methods)只是用略有不同的参数来调用同一个公共方法(common method):
- (void)_prepClosureCIF { _closureArgCount = [self _prepCIF: &_closureCIF withEncodeString: BlockSig(_block) skipArg: YES]; }
- (void)_prepInnerCIF { [self _prepCIF: &_innerCIF withEncodeString: BlockSig(_block) skipArg: NO]; }-_prepCIF:withEncodeString:skipArg: 方法会进而调用另一个方法,由该方法真正完成将 @encode 字符串转换为 ffi_type 数组的工作。随后,该方法会根据需要跳过第一个参数,并调用 ffi_prep_cif 来填充 ffi_cif 结构体:
- (int)_prepCIF: (ffi_cif *)cif withEncodeString: (const char *)str skipArg: (BOOL)skip { int argCount; ffi_type **argTypes = [self _argsWithEncodeString: str getCount: &argCount;];
if(skip) { argTypes++; argCount--; }
ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, argCount, [self _ffiArgForEncode: str], argTypes); if(status != FFI_OK) { NSLog(@"Got result %ld from ffi_prep_cif", (long)status); abort(); }
return argCount; }Foundation 提供了一个便捷的函数 NSGetSizeAndAlignment,在解析这类字符串时非常有用。当传入一个 @encode 字符串时,它会返回该字符串中第一个类型的大小和对齐方式,并返回一个指向下一个类型的指针。理论上,我们只需在循环中调用此函数,就能遍历一个 block 签名中的所有类型。
实际上,这里有个复杂情况。出于我从未查明的原因,方法签名(method signatures,也因此包括 block 签名)中,各个类型编码之间会夹杂一些数字。NSGetSizeAndAlignment 对这些数字一无所知,因此需要适当调整才能正确解析这类字符串。我编写了一个辅助函数,它调用 NSGetSizeAndAlignment 后,会跳过在类型字符串之后找到的任何数字:
static const char *SizeAndAlignment(const char *str, NSUInteger *sizep, NSUInteger *alignp, int *len) { const char *out = NSGetSizeAndAlignment(str, sizep, alignp); if(len) *len = out - str; while(isdigit(*out)) out++; return out; } static int ArgCount(const char *str) { int argcount = -1; // return type is the first one while(str && *str) { str = SizeAndAlignment(str, NULL, NULL, NULL); argcount++; } return argcount; } - (ffi_type **)_argsWithEncodeString: (const char *)str getCount: (int *)outCount { int argCount = ArgCount(str); ffi_type **argTypes = [self _allocate: argCount * sizeof(*argTypes)]; int i = -1; while(str && *str) { const char *next = SizeAndAlignment(str, NULL, NULL, NULL); if(i >= 0) argTypes[i] = [self _ffiArgForEncode: str]; i++; str = next; } *outCount = argCount;
return argTypes; } - (ffi_type *)_ffiArgForEncode: (const char *)str {libffi(外部函数接口库)按大小区分整数类型,并没有与 int 或 long 直接对应的类型。为帮助我在这两者之间进行转换,我构建了一些宏。(事实证明 libffi 也为此内置了一些宏,比如 ffi_type_sint 这样的 #define 会映射到正确的基础 ffi_type。我在编写代码时并不知道这些,因此我的方法比必要情况稍显迂回。)
如前所述,基本类型在 @encode 中用单个字符表示。为避免硬编码任何字符值,我使用类似 @encode(type)[0] 的表达式来获取该单个字符。如果它等于 str[0],则说明字符串编码的就是该基本类型。
我的有符号整数宏首先执行此检查以判断类型是否匹配。若匹配,它会使用 sizeof(type) 来确定所讨论的整数类型的大小,并返回与之匹配的相应 ffi_type *(指向类型描述结构的指针)。宏的示例如下:
#define SINT(type) do { \ if(str[0] == @encode(type)[0]) \ { \ if(sizeof(type) == 1) \ return &ffi;_type_sint8; \ else if(sizeof(type) == 2) \ return &ffi;_type_sint16; \ else if(sizeof(type) == 4) \ return &ffi;_type_sint32; \ else if(sizeof(type) == 8) \ return &ffi;_type_sint64; \ else \ { \ NSLog(@"Unknown size for type %s", #type); \ abort(); \ } \ } \ } while(0) #define UINT(type) do { \ if(str[0] == @encode(type)[0]) \ { \ if(sizeof(type) == 1) \ return &ffi;_type_uint8; \ else if(sizeof(type) == 2) \ return &ffi;_type_uint16; \ else if(sizeof(type) == 4) \ return &ffi;_type_uint32; \ else if(sizeof(type) == 8) \ return &ffi;_type_uint64; \ else \ { \ NSLog(@"Unknown size for type %s", #type); \ abort(); \ } \ } \ } while(0)作为对整数宏的补充,这里有一个快捷方式,它接受一个整数类型,然后生成检查该类型有符号与无符号变体的代码:
#define INT(type) do { \ SINT(type); \ UINT(unsigned type); \ } while(0) #define COND(type, name) do { \ if(str[0] == @encode(type)[0]) \ return &ffi_type_ ## name; \ } while(0) #define PTR(type) COND(type, pointer)理论上,可以通过解析 @encode 字符串中的结构体定义并构建匹配的 ffi_type(libffi 类型描述符)来支持任意结构体。但在实践中,这既困难又容易出错 ——@encode 格式本身并不友好。对于处理大多数情况而言,只需翻译一小部分结构体。这些结构体可通过简单的字符串比较来识别,无需解析 @encode 字符串,随后只需为 libffi 提供一个硬编码的类型列表即可。虽然此方法无法覆盖所有场景,但若遇到未知结构体可立即中止并提示,且便于添加新类型,这使得开发者能快速修复可能遇到的缺陷。
最后一个宏用于处理结构体。它接收一个结构体类型及对应的 ffi_types 列表。若 @encode 匹配,则为该结构体创建一个 ffi_type,根据传入的参数填充其元素字段,并返回该类型:
#define STRUCT(structType, ...) do { \ if(strncmp(str, @encode(structType), strlen(@encode(structType))) == 0) \ { \ ffi_type *elementsLocal[] = { __VA_ARGS__, NULL }; \ ffi_type **elements = [self _allocate: sizeof(elementsLocal)]; \ memcpy(elements, elementsLocal, sizeof(elementsLocal)); \ \ ffi_type *structType = [self _allocate: sizeof(*structType)]; \ structType->type = FFI_TYPE_STRUCT; \ structType->elements = elements; \ return structType; \ } \ } while(0) SINT(_Bool); SINT(signed char); UINT(unsigned char); INT(short); INT(int); INT(long); INT(long long); PTR(id); PTR(Class); PTR(SEL); PTR(void *); PTR(char *); PTR(void (*)(void)); COND(float, float); COND(double, double);
COND(void, void);现在处理基本类型的问题解决了,接下来是结构体。我只处理 CGRect、CGPoint、CGSize 及其对应的 NS 等价类型。如果需要的话,其他结构体也可以很容易地添加进来。
这些结构体的元素类型都是 CGFloat。CGFloat 的类型可以是 float 或 double,具体取决于平台。因此,首先要确定它属于哪一种,然后获取相应的 ffi_type:
ffi_type *CGFloatFFI = sizeof(CGFloat) == sizeof(float) ? &ffi;_type_float : &ffi;_type_double; STRUCT(CGRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI); STRUCT(CGPoint, CGFloatFFI, CGFloatFFI); STRUCT(CGSize, CGFloatFFI, CGFloatFFI); #if !TARGET_OS_IPHONE STRUCT(NSRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI); STRUCT(NSPoint, CGFloatFFI, CGFloatFFI); STRUCT(NSSize, CGFloatFFI, CGFloatFFI); #endif NSLog(@"Unknown encode string %s", str); abort(); }当闭包(closure)被准备时,它需要三个关键的数据。其一是之前代码费力构建的类型信息(type information)。其二是以 libffi 格式接收参数的 C 函数。其三是传递给该 C 函数的上下文指针(context pointer)。正是这个上下文指针让所有魔法得以发生 —— 它使函数能够判断当前调用关联的是 MABlockClosure 的哪个实例,并最终调用到关联的代码块(block)。
与闭包分配和释放类似,闭包的准备方式取决于 libffi 正在运行的模式。如果 libffi 自行管理闭包内存分配,那么只需单次调用即可完成闭包准备。否则,需要先通过一次调用进行设置,再通过一次 mprotect 调用将内存标记为可执行。以下是 -_prepClosure 方法的实现:
- (void)_prepClosure { #if USE_LIBFFI_CLOSURE_ALLOC ffi_status status = ffi_prep_closure_loc(_closure, &_closureCIF, BlockClosure, self, _closureFptr); if(status != FFI_OK) { NSLog(@"ffi_prep_closure returned %d", (int)status); abort(); } #else ffi_status status = ffi_prep_closure(_closure, &_closureCIF, BlockClosure, self); if(status != FFI_OK) { NSLog(@"ffi_prep_closure returned %d", (int)status); abort(); }
if(mprotect(_closure, sizeof(_closure), PROT_READ | PROT_EXEC) == -1) { perror("mprotect"); abort(); } #endif } static void BlockClosure(ffi_cif *cif, void *ret, void **args, void *userdata) { MABlockClosure *self = userdata; int count = self->_closureArgCount; void **innerArgs = malloc((count + 1) * sizeof(*innerArgs)); innerArgs[0] = &self-;>_block; memcpy(innerArgs + 1, args, count * sizeof(*args)); ffi_call(&self-;>_innerCIF, BlockImpl(self->_block), ret, innerArgs); free(innerArgs); }直接使用 MABlockClosure 稍显不便。我编写了两个便捷函数来简化这一操作。BlockFptr 函数会在代码块(block)本身上创建一个 MABlockClosure 实例作为关联对象(associated object)。这确保了只要代码块有效,函数指针就保持有效:
void *BlockFptr(id block) { @synchronized(block) { MABlockClosure *closure = objc_getAssociatedObject(block, BlockFptr); if(!closure) { closure = [[MABlockClosure alloc] initWithBlock: block]; objc_setAssociatedObject(block, BlockFptr, closure, OBJC_ASSOCIATION_RETAIN); [closure release]; // retained by the associated object assignment } return [closure fptr]; } } void *BlockFptrAuto(id block) { return BlockFptr([[block copy] autorelease]); } int x = 42; void (*fptr)(void) = BlockFptrAuto(^{ NSLog(@"%d", x); }); fptr(); // prints 42!以上就是本期(延迟的)周五问答的全部内容。下期再见,届时我会继续分享更多探讨主题。一如既往,欢迎大家随时将想探讨的话题建议发送给我。
Original (English)
Source: https://www.mikeash.com/pyblog/friday-qa-2011-05-06-a-tour-of-mablockclosure.html
It’s a week late, but it’s finally time for the latest edition of Friday Q&A. About a year ago, I wrote about converting blocks into function pointers by building code at runtime. This was an interesting exercise, but ultimately impractical due to various limitations. In the meantime, I wrote MABlockClosure, a more robust and usable way of doing the same thing, but I never posted about it. Landon Fuller suggest I discuss how it works, and so that is what I will talk about today.
Recap Blocks are an extremely useful language feature for two reasons: they allow writing anonymous functions inlined in other code, and they can capture context from the enclosing scope by referring to local variables from that scope. Among other things, this makes callback patterns much simpler. Instead of this:
struct CallbackContext { NSString *title; int value; };
static void MyCallback(id result, void *contextVoid) { struct CallbackContext *context = contextVoid; // use result, context->title, and context->value }
struct CallbackContext ctx; ctx.title = [self title]; ctx.value = [self value]; CallAPIWithCallback(workToDo, MyCallback, &ctx;); CallAPIWithCallbackBlock(workToDo, ^(id result) { // use result, [self title], [self value] });The problem is that not all callbacks-based APIs have versions that take blocks. What MABlockClosure and my older experimental trampoline code allow is converting a block to a function pointer that can be passed to one of these APIs. For example, if CallAPIWithCallbackBlock didn’t exist, MABlockClosure allows writing code that’s nearly as nice:
CallAPIWithCallback(workToDo, BlockFptrAuto(^(id result) { // use result, [self title], [self value] }));Blocks ABI Blocks compile down to a function and a couple of structs. The function holds the code, and the structs hold information about the block, including the captured context. The function contains an implicit argument, much like the self argument to Objective-C methods, which points to the block structure. The block above translates to something like this:
void BlockImpl(struct BlockStruct *block, id info) { // code goes here }My original attempt used a small bit of assembly code for the trampoline. This code tried to shift the arguments in a general fashion, and then insert the pointer at the front. Unfortunately, this really can’t be done by the same code for all cases, so it ended up with a lot of irritating restrictions.
At the time, this was about the best that could be done. Fortunately, Apple later added type metadata to blocks. As long as you’re using a compiler that’s recent enough to generate this metadata (any recent clang will do), this can be used to generate intelligent trampolines which do the appropriate argument manipulation.
libffi Although the block type metadata provides all of the necessary information needed to perform the necessary argument transformation, it’s still an extremely complicated undertaking. The exact nature of what needs to be done depends heavily on the function call ABI of the particular architecture the code is running on, and the particular argument types present.
If I had to do all of this myself, I never would have been able to put in the enormous effort required. The good news is that there is a library already built which knows how to handle all of this for a whole bunch of different architectures: libffi.
libffi provides two major facilities. It’s best known for the ability to call into an arbitrary function with arbitrary arguments whose types aren’t known until runtime. A lesser-known facility provides what is essentially the opposite: it allows creating “closures” which are runtime-generated functions which capture arbitrary arguments whose types aren’t known until runtime.
The latter is what we need to generate the trampoline function for the block. This captures the arguments in a form that can be manipulated from C code. That code can then manipulate the arguments as needed and use the former facility to call the block’s implementation pointer.
Support Structures The layout of a block structure is not in any published header. However, since these structures are baked into executables when they’re compiled, we can safely extract them from the specification and rely on that to match.
These are the structures in question:
struct BlockDescriptor { unsigned long reserved; unsigned long size; void *rest[1]; };
struct Block { void *isa; int flags; int reserved; void *invoke; struct BlockDescriptor *descriptor; }; static void *BlockImpl(id block) { return ((struct Block *)block)->invoke; } static const char *BlockSig(id blockObj) { struct Block *block = (void *)blockObj; struct BlockDescriptor *descriptor = block->descriptor;
int copyDisposeFlag = 1 << 25; int signatureFlag = 1 << 30;
assert(block->flags & signatureFlag);
int index = 0; if(block->flags & copyDisposeFlag) index += 2;
return descriptor->rest[index]; }A lot of the necessary libffi data structures have to be created dynamically depending on the type signature. Manually managing that memory gets irritating. Since their lifetime is tied to the life of the closure object itself, the simplest way to deal with this is to track allocations in the object. To do this, I have an NSMutableArray. When I need to allocate memory, I create an NSMutableData of the appropriate size, add it to this array, and then return its mutableBytes pointer. This array is the class’s first instance variable:
@interface MABlockClosure : NSObject { NSMutableArray *_allocations; ffi_cif _closureCIF; ffi_cif _innerCIF; int _closureArgCount; ffi_closure *_closure; void *_closureFptr; id _block; } - (id)initWithBlock: (id)block;
- (void *)fptr;
@end - (void *)fptr { return _closureFptr; } - (id)initWithBlock: (id)block { if((self = [self init])) { _allocations = [[NSMutableArray alloc] init]; _block = block; _closure = AllocateClosure(&_closureFptr); [self _prepClosureCIF]; [self _prepInnerCIF]; [self _prepClosure]; } return self; }Newer versions of libffi encapsulate all of this in calls to allocate, prepare, and deallocate closures. This is what you’ll get if you build libffi from source, and it’s what you can get on iOS. MABlockClosure is built to handle both ways.
The AllocateClosure function uses conditional compilation to decide which technique to use. If USE_LIBFFI_CLOSURE_ALLOC is set, it just calls through to libffi. Otherwise, it allocates the memory using mmap, which ensures that the memory is properly aligned and can later be marked executable. Here’s what that function looks like:
static void *AllocateClosure(void **codePtr) { #if USE_LIBFFI_CLOSURE_ALLOC return ffi_closure_alloc(sizeof(ffi_closure), codePtr); #else ffi_closure *closure = mmap(NULL, sizeof(ffi_closure), PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if(closure == (void *)-1) { perror("mmap"); return NULL; } *codePtr = closure; return closure; #endif } static void DeallocateClosure(void *closure) { #if USE_LIBFFI_CLOSURE_ALLOC ffi_closure_free(closure); #else munmap(closure, sizeof(ffi_closure)); #endif }The two prep methods called by -initWithBlock: just call through to a single common method with slightly different arguments:
- (void)_prepClosureCIF { _closureArgCount = [self _prepCIF: &_closureCIF withEncodeString: BlockSig(_block) skipArg: YES]; }
- (void)_prepInnerCIF { [self _prepCIF: &_innerCIF withEncodeString: BlockSig(_block) skipArg: NO]; }The -_prepCIF:withEncodeString:skipArg: method in turn calls through to another method which does the real work of the conversion of the @encode string to an array of ffi_type. It then skips over the first argument if needed, and calls ffi_prep_cif to fill out the ffi_cif struct:
- (int)_prepCIF: (ffi_cif *)cif withEncodeString: (const char *)str skipArg: (BOOL)skip { int argCount; ffi_type **argTypes = [self _argsWithEncodeString: str getCount: &argCount;];
if(skip) { argTypes++; argCount--; }
ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, argCount, [self _ffiArgForEncode: str], argTypes); if(status != FFI_OK) { NSLog(@"Got result %ld from ffi_prep_cif", (long)status); abort(); }
return argCount; }Foundation provides a handy function called NSGetSizeAndAlignment which helps a great deal when parsing these strings. When passed an @encode string, it returns the size and alignment of the first type in the string, and returns a pointer to the next type. In theory, we can iterate through the types in a block signature by just calling this function in a loop.
In practice, there’s a complication. For reasons I have never discovered, method signatures (and thus block signatures) have numbers in between the individual type encodings. NSGetSizeAndAlignment is clueless about these, so it needs a bit of help to correctly parse one of these strings. I wrote a small helper function which calls NSGetSizeAndAlignment and then skips over any digits it finds after the type string:
static const char *SizeAndAlignment(const char *str, NSUInteger *sizep, NSUInteger *alignp, int *len) { const char *out = NSGetSizeAndAlignment(str, sizep, alignp); if(len) *len = out - str; while(isdigit(*out)) out++; return out; } static int ArgCount(const char *str) { int argcount = -1; // return type is the first one while(str && *str) { str = SizeAndAlignment(str, NULL, NULL, NULL); argcount++; } return argcount; } - (ffi_type **)_argsWithEncodeString: (const char *)str getCount: (int *)outCount { int argCount = ArgCount(str); ffi_type **argTypes = [self _allocate: argCount * sizeof(*argTypes)]; int i = -1; while(str && *str) { const char *next = SizeAndAlignment(str, NULL, NULL, NULL); if(i >= 0) argTypes[i] = [self _ffiArgForEncode: str]; i++; str = next; } *outCount = argCount;
return argTypes; } - (ffi_type *)_ffiArgForEncode: (const char *)str {libffi differentiates integer types by size, and has no direct equivalent to int or long. To help me convert between the two, I built some macros. (It turns out that libffi built some macros for this as well. There are #defines like ffi_type_sint which map to the correct base ffi_type. I didn’t know about these when I wrote the code, so my method is slightly more roundabout than it needs to be.)
As I mentioned earlier, primitives are represented as single characters in an @encode. To avoid hardcoding any of those character values, I use an expression like @encode(type)[0] to get that single character. If this equals str[0], then that’s the primitive type encoded by the string.
My macro for signed integers first performs this check to see if the types match. If they do, it then uses sizeof(type) to figure out how big the integer type in question is and return the appropriate ffi_type * to match. Here’s what the macro looks like:
#define SINT(type) do { \ if(str[0] == @encode(type)[0]) \ { \ if(sizeof(type) == 1) \ return &ffi;_type_sint8; \ else if(sizeof(type) == 2) \ return &ffi;_type_sint16; \ else if(sizeof(type) == 4) \ return &ffi;_type_sint32; \ else if(sizeof(type) == 8) \ return &ffi;_type_sint64; \ else \ { \ NSLog(@"Unknown size for type %s", #type); \ abort(); \ } \ } \ } while(0) #define UINT(type) do { \ if(str[0] == @encode(type)[0]) \ { \ if(sizeof(type) == 1) \ return &ffi;_type_uint8; \ else if(sizeof(type) == 2) \ return &ffi;_type_uint16; \ else if(sizeof(type) == 4) \ return &ffi;_type_uint32; \ else if(sizeof(type) == 8) \ return &ffi;_type_uint64; \ else \ { \ NSLog(@"Unknown size for type %s", #type); \ abort(); \ } \ } \ } while(0)To round out the integer macros, I have a quick one which takes an integer type and then generates code to check for both signed and unsigned variants:
#define INT(type) do { \ SINT(type); \ UINT(unsigned type); \ } while(0) #define COND(type, name) do { \ if(str[0] == @encode(type)[0]) \ return &ffi_type_ ## name; \ } while(0) #define PTR(type) COND(type, pointer)In theory, it would be possible to support arbitrary structs by parsing the struct in the @encode string and building up the appropriate ffi_type to match. In practice, this is difficult and error-prone. The @encode format is not very friendly at all. To handle most cases, there are only a small number of structs that need to be translated. These structs can be detected with a simple string compare without parsing the @encode string, and then a simple hardcoded list of types provided to libffi. While this won’t handle all cases, by bailing out early if an unknown struct is discovered and making it easy to add new ones, this enables the programmer to quickly fix any deficiences which may be encountered.
One last macro handles structs. It takes a struct type and a list of corresponding ffi_types. If the @encode matches, it creates an ffi_type for the struct, fills out the elements from the arguments given, and returns it:
#define STRUCT(structType, ...) do { \ if(strncmp(str, @encode(structType), strlen(@encode(structType))) == 0) \ { \ ffi_type *elementsLocal[] = { __VA_ARGS__, NULL }; \ ffi_type **elements = [self _allocate: sizeof(elementsLocal)]; \ memcpy(elements, elementsLocal, sizeof(elementsLocal)); \ \ ffi_type *structType = [self _allocate: sizeof(*structType)]; \ structType->type = FFI_TYPE_STRUCT; \ structType->elements = elements; \ return structType; \ } \ } while(0) SINT(_Bool); SINT(signed char); UINT(unsigned char); INT(short); INT(int); INT(long); INT(long long); PTR(id); PTR(Class); PTR(SEL); PTR(void *); PTR(char *); PTR(void (*)(void)); COND(float, float); COND(double, double);
COND(void, void);That takes care of primitives. Now it’s time for structs. I only handle CGRect, CGPoint, CGSize, and their NS equivalents. Others could easily be added if necessary.
These structs all have elements of type CGFloat. The type of CGFloat can either be float or double depending on the platform. The first thing to do, then, is to figure out which one it is, and grab the corresponding ffi_type:
ffi_type *CGFloatFFI = sizeof(CGFloat) == sizeof(float) ? &ffi;_type_float : &ffi;_type_double; STRUCT(CGRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI); STRUCT(CGPoint, CGFloatFFI, CGFloatFFI); STRUCT(CGSize, CGFloatFFI, CGFloatFFI); #if !TARGET_OS_IPHONE STRUCT(NSRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI); STRUCT(NSPoint, CGFloatFFI, CGFloatFFI); STRUCT(NSSize, CGFloatFFI, CGFloatFFI); #endif NSLog(@"Unknown encode string %s", str); abort(); }When a closure is prepared, it takes three important pieces of data. One is the type information that all of the previous code worked so hard to build. One is a C function which receives the arguments in libffi format. The last one is a context pointer which is passed into that C function. This context pointer is what allows all of the magic to happen. It allows the function to determine which instance of MABlockClosure the call is associated with, and call through to the associated block.
Like with closure allocation and deallocation, how the closure is prepared depends on which mode libffi is operating in. If libffi is managing its own closure allocation, then it’s just a single call to prepare the closure. Otherwise, there’s a different call to set it up, and then a call to mprotect is required to mark the memory as executable. Here’s what the -_prepClosure method looks like:
- (void)_prepClosure { #if USE_LIBFFI_CLOSURE_ALLOC ffi_status status = ffi_prep_closure_loc(_closure, &_closureCIF, BlockClosure, self, _closureFptr); if(status != FFI_OK) { NSLog(@"ffi_prep_closure returned %d", (int)status); abort(); } #else ffi_status status = ffi_prep_closure(_closure, &_closureCIF, BlockClosure, self); if(status != FFI_OK) { NSLog(@"ffi_prep_closure returned %d", (int)status); abort(); }
if(mprotect(_closure, sizeof(_closure), PROT_READ | PROT_EXEC) == -1) { perror("mprotect"); abort(); } #endif } static void BlockClosure(ffi_cif *cif, void *ret, void **args, void *userdata) { MABlockClosure *self = userdata; int count = self->_closureArgCount; void **innerArgs = malloc((count + 1) * sizeof(*innerArgs)); innerArgs[0] = &self-;>_block; memcpy(innerArgs + 1, args, count * sizeof(*args)); ffi_call(&self-;>_innerCIF, BlockImpl(self->_block), ret, innerArgs); free(innerArgs); }Convenience Functions Using MABlockClosure directly is slightly inconvenient. I built two convenience functions to make this a bit easier. The BlockFptr function creates an MABlockClosure instance as an associated object on the block itself. This ensures that the function pointer remains valid for as long as the block is valid:
void *BlockFptr(id block) { @synchronized(block) { MABlockClosure *closure = objc_getAssociatedObject(block, BlockFptr); if(!closure) { closure = [[MABlockClosure alloc] initWithBlock: block]; objc_setAssociatedObject(block, BlockFptr, closure, OBJC_ASSOCIATION_RETAIN); [closure release]; // retained by the associated object assignment } return [closure fptr]; } } void *BlockFptrAuto(id block) { return BlockFptr([[block copy] autorelease]); } int x = 42; void (*fptr)(void) = BlockFptrAuto(^{ NSLog(@"%d", x); }); fptr(); // prints 42!That wraps up this week’s (late) Friday Q&A. Come back in two weeks for the next installment. Until then, as always, keep sending me your ideas for topics to cover here.