Introduction to the
JSPatch recently added the JPBlock extension to address some of the limitations of previous block usage:
- A maximum of six block parameters are supported.
- Block argument types cannot be double/struct/union.
- JS wrapped blocks cannot be passed to OC and then passed back to JS to call.
Once connected to JPBlock, you can:
-
Any number of block arguments can be supported.
-
Block argument types can be any type other than struct/union, including double.
-
Support JS wrapped blocks to OC and back to JS to call.
This article describes the implementation principle here.
The original implementation
Function (OC) : Function (OC) : Function (OC) : Function (OC) : Function (OC) : Function (OC)
- Block -> JSFunction, from OC to JS can be called as JSFunction.
- JSFunction -> Block, Function from JS to OC can be called as Block.
JavaScriptCore returns a block to JS via the JavaScriptCore interface, and JavaScriptCore automatically converts it to a JS Function, but there is a pit there, which I’ll talk about later.
For the second point, JavaScriptCore is not processed. A JS Function passed into OC through JavaScriptCore is still a JSValue type, which needs to be processed by ourselves. Specific usage scenarios:
@implementation JPDemoClass
+ (void)callBlock:(void(^)(NSString *str))blk {
blk(@"string from oc");
}
@end
require('JPDemoClass').callBlock(block("NSString *", function(str){
console.log(str);
});
Copy the code
After JS passes in a Function to OC, which is processed by the JSPatch engine, it can be called as a block. So what does JSPatch do here? JSPatch creates a block as a diversion:
static id genCallbackBlock(JSValue *jsVal) { id cb = ^id(void *p0, void *p1, void *p2, void *p3, void *p4, Void *p5) {// cast parameters // call JS function in jsVal}; return cb; }Copy the code
In the above example, BLK (@ “string from OC”) calls a block dynamically created by JSPatch. This block holds the Function passed in from JS, and extracts arguments to call JS Function. Return the result of the JS Function execution, and the switch is complete.
One of the tricky questions here is, how do you dynamically create blocks with different parameter types? In the original implementation, JSPatch used a crafty and simple way to create a block that returns type ID, takes six parameters, and is of type void * to represent all blocks.
Void star is an untyped pointer, it can represent any data type, NSObject is a pointer, void star can be cast to NSObject, void star can be cast to int/BOOL, In addition, you can force a block with many arguments to be called as a block with few arguments, because the memory structure is the same, as long as the block does not fetch arguments that are not passed, it is ok.
So a single block here can represent all blocks that return type ID and have 0 to 6 arguments.
The problem
It is also clear why the original implementation is limited to the first half of the above point. The first point is that there are only six parameters declared here, and the number of parameters can not be handled any more. Of course, it can be added up to a dozen. The second thing is because void* can’t represent double, it can’t cast, struct/union can’t cast.
What is the third problem (do not support JS wrapped block to OC and then back JS to call)? JavaScriptCore does not automatically convert it into A JS Function. JavaScriptCore is not a black box. We can see the exact reason why JavaScriptCore converts blocks. Can be seen from objCCallbackFunctionForBlock this function, back in the end, can be in parseObjCType () found here if you have any pointer/Class/block parameters in the union type, such as is not automatic conversion. The block arguments we generate here are all void * Pointers and will not be converted.
What is the block
As long as you can dynamically create blocks with different parameter types, you can solve the above three problems in one go. So how to create it, first of all need to understand what block is, there are a lot of block principle analysis articles on the Internet, you can have a look, will not elaborate, here simply say:
- Block = function + data. A block is a function that holds some data.
- Block definitions and calls, when compiled, generate structures and function Pointers that hold data.
The complexity of a block lies in the handling of variables. If you ignore this part of the handling, the structure of a block is very simple. You can create a block directly from the structure:
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 JPSimulateBlock2 simulateBlock = {
&_NSConcreteStackBlock,
0, 0, blockImp, &descriptor
};
void *blockPtr = &simulateBlock;
void (^blk)() = ((__bridge id)blockPtr);
blk(); //output "call block succ"
}
Copy the code
A particular structure that holds a pointer to a function is a block, and calling that block is calling the function to which the pointer points. The type and number of arguments in a block are independent of the structure. The structure can be used to represent the block regardless of the type and number of arguments in the block. The difference is that the function pointer must point to the corresponding function of the parameter type.
Ps. IOS development exchange technology group: welcome to join, no matter you are big or small white welcome to enter, share BAT, Ali interview questions, interview experience, discuss technology, we exchange learning and growth together
So if we want to dynamically create blocks of arbitrary parameter types, the question becomes how do we create C functions that support arbitrary parameter types. How do we do that? Two ways:
- Permutation and combination of all parameter types and numbers, static declaration of N functions, at run time according to the number of parameter types allocated corresponding functions.
- Dynamically define the corresponding function based on the parameter type and number.
Obviously the second way is the right way, and the question becomes how to define a function dynamically.
Dynamically defined function
If you’ve seen How to call C functions dynamically, this problem may have a familiar feeling. This is exactly what Libffi does well. The principle is the same as calling C functions dynamically, except that the principle is defined instead of being repeated here. Libffi also supports dynamic definition of C functions at run time, simulating function arguments on and off the stack to complete the call. Here’s how it works:
svoid JPBlockInterpreter(ffi_cif *cif, void *ret, void **args, Void * userData) {//① // function entity // Extract parameters from userdata/args // return values to 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); }Copy the code
- Prepare a function entity JPBlockInterpreter.
- A cif object is assembled based on the number of function arguments/parameter types/return value types to represent this function prototype.
- Prepare a function pointer, blockImp, for the call.
- Use ffi_closure to associate function prototype _cifPtr/function entity JPBlockInterpreter/context object self/function pointer blockImp.
In the example above, blockImp can be called as a function that returns a value of type void and arguments of type (int A, NSString *b) to the JPBlockInterpreter function entity, In this function, you can use args to fetch the parameters passed in, and userData to fetch the context for processing. Different CIF objects can be dynamically created based on different parameter types to generate different blockImp function Pointers.
Libffi provides an interface to dynamically define functions of any parameter type. You can then dynamically create blocks of any type. All that is left to do is to define the interface. Access the JSPatch process and so on.
signature
I would have ended there, but there is a question of signatures. JavaScriptCore does not automatically convert a block created in the above way to JS Function when it is passed out to javascript. Can be in objCCallbackFunctionForBlock () this method:
if (! _Block_has_signature(target)) return 0;Copy the code
If a block is passed without a signature, the conversion logic will be followed. The answer can be found in the implementation of _Block_has_signature() in the Runtime source code. The block structure we just defined looks like this:
struct JPSimulateBlock {
void *isa;
int flags;
int reserved;
void *invoke;
struct JPSimulateBlockDescriptor *descriptor;
};
struct JPSimulateBlockDescriptor {
unsigned long int reserved;
unsigned long int size;
};
Copy the code
The Descriptor descriptor has only reserved and size data. In fact, the descriptor descriptor will append data as needed. There are three groups of descriptor descriptors defined in the Runtime:
struct JPSimulateBlockDescriptor {
//Block_descriptor_1
struct {
unsigned long int reserved;
unsigned long int size;
};
//Block_descriptor_2
struct {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
//Block_descriptor_3
struct {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout;
};
};
Copy the code
Block flags have two bits: BLOCK_HAS_COPY_DISPOSE(1 << 25) and BLOCK_HAS_SIGNATURE(1 << 30), which indicate whether the block descriptor is descriptor The two groups of data, Block_descriptor_2 and Block_descriptor_3. The block’s signature is stored in the Block_descriptor_3 structure. So if we want block to have signature, we need:
- Block flags must contain the BLOCK_HAS_SIGNATURE flag to indicate that the block has signature data.
- Keep the signature data into JPSimulateBlockDescriptor descriptor pointing to the structure of the body.
The signature data represents the block return type/parameter type, something like this: “i8@? @ 8 “. So you just do it according to its rules, and you can see that in JPBlock, the final descriptor and the block will look something like this:
struct JPSimulateBlockDescriptor descriptor = {
0,
sizeof(struct JPSimulateBlock),
[self.signature.types cStringUsingEncoding:NSUTF8StringEncoding], //signature数据
NULL
};
....
struct JPSimulateBlock simulateBlock = {
&_NSConcreteStackBlock,
BLOCK_HAS_SIGNATURE,
0,
blockImp,
&descriptor
};
....
Copy the code
With signature, JavaScriptCore can also automatically convert blocks created here to JS functions.