📜 ⬆️ ⬇️

What brings the idea (Objective-C) - target-action on the blocks and a lot of runtime

An idea came to me somehow, is it possible to take a block and give it to target-action?
There are ready-made solutions, like BlocksKit and other libraries, for example, but their solution is to save the block, install the target and call the block from the specified selector.

Why, then, need this article?

I wanted to create a way to generate a selector, which will be called block. What is difficult here, you say? imp_implementationWithBlock + class_addMethod and the case is closed. But with this approach, there is one serious requirement; this is the first argument of the block — the owner of the method.
')
How to get around this requirement and do this?
[button addTarget:self action:[self ax_lambda:^(UIButton *sender, UIEvent *event){ NSLog(@"click on button %@, event = %@", sender, event); }] forControlEvents:UIControlEventTouchUpInside]; [button addTarget:self action:[self ax_lambda:^{ NSLog(@"click"); }] forControlEvents:UIControlEventTouchUpInside]; 

Or even like this
  __block NSInteger sum = 0; [self performSelector:[self ax_lambda:^(NSNumber *argA, NSNumber *argB) { sum = [argA integerValue] + [argB integerValue]; }] withObject:@(2) withObject:@(3)]; //sum — 5 SEL selSum = [self ax_lambda:^NSInteger(NSInteger argA, NSInteger argB){ return argA + argB; }]; NSInteger(*funcSum)(id, SEL, NSInteger, NSInteger) = (NSInteger(*)(id, SEL, NSInteger, NSInteger))objc_msgSend; NSInteger sum2 = funcSum(self, selSum, 2, 3); //sum2 — 5 

The implementation was so interesting that I decided to write about it.

In essence, the main task is to get rid of the first self argument in the block call. This is the root problem of the entire solution (it is a pity that it is not the only one).
Earlier, I already wrote a little about blocks , and noted that a block is an object, which means the call will occur through NSInvocation.
If I get the moment of calling the block and remove the argument self (by shifting the arguments) in NSInvocation, then I will get the desired result.

Further it will be necessary to understand along the way.


AXProxyBlock


The question is how to break in at the time of the block call? How to get a block call at all?
Very often I write this phrase, but a block is an object. The object in the objc in the final form is a structure. Once id is a pointer to a structure, the opposite is allowed (__bridge, hello) .
It turns out, you can create a fake unit. Well, or a proxy for the block.

The interface of my class is the following:
 typedef void(^AXProxyBlockInterpose)(NSInvocation *invocation); @interface AXProxyBlock : NSProxy + (instancetype)initWithBlock:(id)block; - (void)setBeforeInvoke:(AXProxyBlockInterpose)beforeInvoke; - (NSString *)blockSignatureStringCTypes; @end 



As you can guess, setBeforeInvoke takes a block in which you can do "magic" transformations of block arguments.
blockSignatureStringCTypes returns the signature of the proxied block. Why is it in the header file? About this later.

link to the documentation page that is about to begin

To get started on the documentation, we will create block structures and a listing with our names.
 typedef struct AXBlockStruct_1 { unsigned long int reserved; unsigned long int size; void (*copy_helper)(void *dst, void *src); void (*dispose_helper)(void *src); const char *signature; } AXBlockStruct_1; typedef struct AXBlockStruct { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct AXBlockStruct_1 *descriptor; } AXBlockStruct; typedef NS_ENUM(NSUInteger, AXBlockFlag) { AXBlockFlag_HasCopyDispose = (1 << 25), AXBlockFlag_HasCtor = (1 << 26), AXBlockFlag_IsGlobal = (1 << 28), AXBlockFlag_HasStret = (1 << 29), AXBlockFlag_HasSignature = (1 << 30) }; 


And now let's do our class.
You need to make the appropriate structure:
 @interface AXProxyBlock () { // isa      NSProxy,     int _flags; int _reserved; IMP _invoke; AXBlockStruct_1 *_descriptor; // ,    ,     AXProxyBlockInterpose _beforeInvoke; id _block; NSMethodSignature *_blockMethodSignature; IMP _impBlockInvoke; } @end 


Now it is necessary that at the time of the call the class imitated the received block:
Matching Field Values
 - (instancetype)initWithBlock:(id)block { if (self != nil) { AXBlockStruct *blockRef = (__bridge AXBlockStruct *)block; _flags = blockRef->flags; _reserved = blockRef->reserved; _descriptor = calloc(1, sizeof(AXBlockStruct_1)); _descriptor->size = class_getInstanceSize([self class]); BOOL flag_stret = _flags & AXBlockFlag_HasStret; _invoke = (flag_stret ? (IMP)_objc_msgForward_stret : (IMP)_objc_msgForward); ... 


Description of the purpose of these fields can be read all on the same page of the clang documentation . Now the fields correspond to the block at the time of the call.

But I have 2 very important ivar, which I did not turn on under the spoiler above, since they already relate to the call of the block and I want to stop on them in more detail.

  _impBlockInvoke = (IMP)blockRef->invoke; _blockMethodSignature = [self blockMethodSignature]; 


_impBlockInvoke is a function of block calling, implementation. This is a normal function pointer and can be called up by hand.
_blockMethodSignature is a block signature method. What it is will be discussed in great detail later.

How to get NSMethodSignature for a block
 - (NSMethodSignature *)blockMethodSignature { const char *signature = [[self blockSignatureStringCTypes] UTF8String]; return [NSMethodSignature signatureWithObjCTypes:signature]; } - (NSString *)blockSignatureStringCTypes { AXBlockStruct *blockRef = (__bridge AXBlockStruct *)_block; const int flags = blockRef->flags; void *signatureLocation = blockRef->descriptor; signatureLocation += sizeof(unsigned long int); signatureLocation += sizeof(unsigned long int); if (flags & AXBlockFlag_HasCopyDispose) { signatureLocation += sizeof(void(*)(void *dst, void *src)); signatureLocation += sizeof(void (*)(void *src)); } const char *signature = (*(const char **)signatureLocation); return [NSString stringWithUTF8String:signature]; } 



We take our block, we get a descriptor from it, then we shift by the required amount to get the block signature (const char *) and create an NSMethodSignature through it. NSMethodSignature determines the number and types of arguments, the return value, and so on.
It looks not difficult, but the manipulations with the flag are confused: depending on the type of block, its signature may be located differently. For example, the global block does not need to shift beyond the functions of copying and destruction.

My class doesn’t have a method to call the block, it means that forwardInvocation will be called, and before it you need to know what type of NSInvocation will be formed, so the methodSignatureForSelector is called, in which we give our _blockMethodSignature.

forwardInvocation
 - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setTarget:_block]; if (_beforeInvoke) { _beforeInvoke(anInvocation); } IMP imp = _impBlockInvoke; [anInvocation invokeUsingIMP:imp]; } 

The code here should be very clear (set a new target for a call, called a block before if exists), but where is the call [anInvocation invoke] ?!
This is black magic. The invokeUsingIMP method is a private API that can be found here, as well as a lot of other things.


Putting the puzzle to continue


I think that the blocking of the material is peculiar, and if you go straight to solving the second half of the problem, fewer people read the article. Therefore, now the wrapper will be glimpsed as picking up a puzzle of ready-made solutions and in the end the second half of the task will be dealt with. This will allow you to relax a little and gather material in a more structured way.

Let's talk about the method that was called at the very beginning of the article - ax_lambda. This is just a category for NSObject, it is a wrapper for calling the main function, which looks like this:
 SEL ax_lambda(id obj, id block, NSMutableArray *lambdas); 

I think now it becomes clearer what the wrapper was written for. And if the first and second argument does not cause questions, then the third makes you wonder. First, I will talk about the need for a third argument, and then I will give a category code for the spoilers.

 SEL ax_lambda(id obj, id block, NSMutableArray *lambdas) { SEL selector = ax_generateFreeSelector(obj); AXProxyBlockWithSelf *proxyBlock = [AXProxyBlockWithSelf initWithBlock:block]; [proxyBlock setBeforeInvoke:^(NSInvocation *invocation){ ax_offsetArgInInvocation(invocation); }]; [lambdas addObject:proxyBlock]; IMP imp = imp_implementationWithBlock(proxyBlock); NSString *signatureString = [proxyBlock blockSignatureStringCTypes]; class_addMethod([obj class], selector, imp, [signatureString UTF8String]); return selector; } 

This is the main function, the very assembled puzzle. The class AXProxyBlockWithSelf will be discussed further, so far I will only note that this is a descendant of the class AXProxyBlock, as you probably guessed.
To make a block with a method, a selector, implementation and string signature are required. The implementation will be obtained from the proxy block, the proxy will also give the string signature (in AXProxyBlock it is the signature of the block being proxied, but in AXProxyBlockWithSelf it is different and this will be discussed later), well, the selector is not difficult to generate. So why the third parameter?

Calling imp_implementationWithBlock will cause a block to be copied (Block_copy). The copy_helper field in the block is a pointer to the block copy function. However, the block proxy does not have this capability. Even if I create a copy function of the form void (*) (void * dst, void * src), I cannot get the desired result. The object to be copied will come to src and it will not be an instance of my class. Therefore, calling imp_implementationWithBlock does not increase the reference count for the proxyBlock object (and proxyBlock will be destroyed after the function is completed). To prevent this, I use a collection that will increase the internal reference count. It turns out the lifetime of the block depends on the lifetime of the collection storing it. In the case of a category, the unit’s lifetime is limited to the owner’s lifetime.

AXLambda.h
 SEL ax_lambda(id obj, id block, NSMutableArray *lambdas); @interface NSObject (AX_Lambda) - (SEL)ax_lambda:(id)block; @end 


AXLambda.m
 static char kAX_NSObjectAssociatedObjectKey; @interface NSObject (_AX_Lambda) @property (copy, nonatomic) NSMutableArray *ax_lambdas; @end @implementation NSObject (_AX_Lambda) @dynamic ax_lambdas; - (void)setAx_lambdas:(NSMutableArray *)lambdas { objc_setAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey, lambdas, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSMutableArray *)ax_lambdas { NSMutableArray *marrey = objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey); if (marrey == nil) { self.ax_lambdas = [NSMutableArray array]; } return objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey); } @end @implementation NSObject (AX_Lambda) - (SEL)ax_lambda:(id)block { return ax_lambda(self, block, self.ax_lambdas); } @end 



Well, the functions used in SEL ax_lambda (id obj, id block, NSMutableArray * lambdas);
SEL ax_generateFreeSelector (id obj)
 SEL ax_generateFreeSelector(id obj) { SEL selector; NSMutableString *mstring = [NSMutableString string]; do { [mstring setString:@"ax_rundom_selector"]; u_int32_t rand = arc4random_uniform(UINT32_MAX); [mstring appendFormat:@"%zd", rand]; selector = NSSelectorFromString(mstring); } while ([obj respondsToSelector:selector]); return selector; } 


void ax_offsetArgInInvocation (NSInvocation * invocation)
 void ax_offsetArgInInvocation(NSInvocation *invocation) { void *foo = malloc(sizeof(void*)); NSInteger arguments = [[invocation methodSignature] numberOfArguments]; for (NSInteger i = 1; i < arguments-1; i++) { //i = 0 is self [invocation getArgument:foo atIndex:i+1]; [invocation setArgument:foo atIndex:i]; } free(foo); } 




We deal with NSMethodSignature on the example of combining stringWithFormat and NSArray


Before you proceed to the next part, you need a basic understanding of how NSInvocation and NSMethodSignature work. I thought to highlight this in a separate article, but I came to the conclusion that if I didn’t go deep into the material, the article would turn out to be interesting and simple (in the analysis of a specific example), but not very large. So I decided to write about it right here.

I needed a method to generate a string from a format and an array of arguments, for example, like this:
  NSString *format = @"%@, foo:%@, hello%@"; NSArray *input = @[@(12), @(13), @" world"]; NSString *result = [NSString ax_stringWithFormat:format array:input]; //result — @"12, foo:13, hello world" 


Unfortunately, the methods I found on SO did not work ( first , second ). Perhaps I did not correctly try to use them (who succeeded — write it down, please) on ARC, but since I needed a working version, I wrote my own implementation.

Without making any detours with pointers or transformations, the decision is based entirely on the principle of how the methods work.

The final form of the method looks like this:
 + (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arguments; 


The standard method for creating a string by format and parameters is as follows.
 - (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0); 

But to use (and the problem itself) you need to create a va_list ( what it is and how to use it ).
The following method is great
+ (instancetype) ax_string: (NSString *) format, ...
 + (instancetype)ax_string:(NSString *)format, ... { va_list list; va_start(list, format); NSString *str = [[NSString alloc] initWithFormat:format arguments:list]; va_end(list); return str; } 


Now the problem is how to call it with arguments from NSArray.

NSInvocation is an object used to store and forward a message between objects and / or between applications.
However, when creating NSInvocation, you must have NSMethodSignature.
NSMethodSignature allows you to determine how many arguments the method takes, the types of arguments, the offsets, the type of the return value. This is a very logical remark from the documentation.
You can’t support the use of variable numbers.

After all, it is not known how many arguments and what type will be passed to the function / method with a variable number of arguments.

And if you still know? If I myself know this information before the call? Then I can say that in this case, the method will take for example 4 arguments and the function will accept a variable number of arguments, this will work.
NSMethodSignature can be created through the generated signature, if you specify all the information above yourself. NSArray contains only pointers and all parameter offsets only by the pointer value, so everything is pretty simple. As I already wrote , you can use self and _cmd in the method because they are implicitly passed to the method.
+ (NSMethodSignature *) ax_generateSignatureForArguments: (NSArray *) arguments
 + (NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)arguments { NSInteger count = [arguments count]; NSInteger sizeptr = sizeof(void *); NSInteger sumArgInvoke = count + 3; // self + _cmd +             NSInteger offsetReturnType = sumArgInvoke * sizeptr; NSMutableString *mstring = [[NSMutableString alloc] init]; [mstring appendFormat:@"@%zd@0:%zd", offsetReturnType, sizeptr]; for (NSInteger i = 2; i < sumArgInvoke; i++) { [mstring appendFormat:@"@%zd", sizeptr * i]; } return [NSMethodSignature signatureWithObjCTypes:[mstring UTF8String]]; } 


It is worth telling a little about what is happening here. First you need to look at the coding types here .
And now, in order, I really hope that you looked at the table.

In the first place of the signature will be the return type and its offset (the return type is after all the arguments, so it will have the maximum offset, but written on the first). Suppose sizeof (void *) is 8 and an array of 3 arguments. But including self + _cmd + the format that will be passed and that we get 6 arguments. 6x8 = 48
@ 48
Then self and _cmd follow. self comes first in arguments for this
@ 48 @ 0: 8
Then format
@ 48 @ 0: 8 @ 16
and arguments
@ 48 @ 0: 8 @ 16 @ 24 @ 32 @ 40

Now, having a signature, you can use NSInvocation
+ (instancetype) ax_stringWithFormat: (NSString *) format array: (NSArray *) arrayArguments
 + (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments { NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setTarget:self]; [invocation setSelector:@selector(ax_string:)]; [invocation setArgument:&format atIndex:2]; for (NSInteger i = 0; i < [arrayArguments count]; i++) { id obj = arrayArguments[i]; [invocation setArgument:(&obj) atIndex:i+3]; } [invocation invoke]; __autoreleasing NSString *string; [invocation getReturnValue:&string]; return string; } 



And now, if you slightly change the method above, you can get rid of the + (instancetype) ax_string method: (NSString *) format, ...
Full code under the spoiler
 + (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments { NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setTarget:self]; [invocation setSelector:@selector(stringWithFormat:)]; [invocation setArgument:&format atIndex:2]; for (NSInteger i = 0; i < [arrayArguments count]; i++) { id obj = arrayArguments[i]; [invocation setArgument:(&obj) atIndex:i+3]; } [invocation invoke]; __autoreleasing NSString *string; [invocation getReturnValue:&string]; return string; } //https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html + (NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)arguments { NSInteger count = [arguments count]; NSInteger sizeptr = sizeof(void *); NSInteger sumArgInvoke = count + 3; //self + _cmd + (NSString *)format NSInteger offsetReturnType = sumArgInvoke * sizeptr; NSMutableString *mstring = [[NSMutableString alloc] init]; [mstring appendFormat:@"@%zd@0:%zd", offsetReturnType, sizeptr]; for (NSInteger i = 2; i < sumArgInvoke; i++) { [mstring appendFormat:@"@%zd", sizeptr * i]; } return [NSMethodSignature signatureWithObjCTypes:[mstring UTF8String]]; } 



Solution of the second half of the problem - how to add 1 more argument to the block unnoticed?



Interception of the moment when the block was called and displacement of arguments was considered. The application code of the idea and the small nuances of this application were considered. However, there is a problem that prevents completion.

A block received in imp_implementationWithBlock must accept the owner arguments first. It turns out that the signature of the input block for the ax_lambda function is different from the required signature, and the arguments in NSInvocation will be passed completely wrong.

The AXProxyBlockWithSelf class redoes the signature of the proxied block, adding an additional first argument to it. Thus, the call to the proxyblock will be completed with the correct arguments, and the first argument will be shifted before the block call itself.
Need to rewrite method - (NSString *) blockSignatureStringCTypes

- (NSString *) blockSignatureStringCTypes
 - (NSString *)blockSignatureStringCTypes { NSString *signature = [super blockSignatureStringCTypes]; NSString *unformatObject = [signature ax_unformatDec]; NSString *formatNewSignature = [self addSelfToFormat:unformatObject]; NSArray *byteSignature = [signature ax_numbers]; NSArray *byteNewSignature = [self changeByteSignature:byteSignature]; return [NSString ax_stringWithFormat:formatNewSignature array:byteNewSignature]; } 


So, there is a block signature, with argument types and offset, return type, and so on.
You need to insert an additional argument into the signature and shift the arguments.
We get the original form of the signature string format
 - (NSString *)ax_unformatDec { NSCharacterSet *characterSet = [NSCharacterSet decimalDigitCharacterSet]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"length > 0"]; NSArray *separated = [[self componentsSeparatedByCharactersInSet:characterSet] filteredArrayUsingPredicate:predicate]; NSString *format = [separated componentsJoinedByString:@"%@"]; if ([[self lastSubstring] isEqualToString:[format lastSubstring]] ) { return format; } else { return [format stringByAppendingString:@"%@"]; } } - (NSString *)lastSubstring { NSInteger lastIndex = [self length] - 1; return [self substringFromIndex:lastIndex]; } 


Next you need to look at the coding types here .
Add the \ "owner \" argument to the first place
 - (NSString *)addSelfToFormat:(NSString *)format { NSMutableArray *marray = [[format componentsSeparatedByString:@"?"] mutableCopy]; [marray insertObject:@"?%@@" atIndex:1]; return [marray componentsJoinedByString:@""]; } 


Get the NSArray argument offsets to call
 - (NSArray *)ax_numbers { NSString *pattern = @"\\d+"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; NSRange fullRange = NSMakeRange(0, [self length]); NSArray *matches = [regex matchesInString:self options:NSMatchingReportProgress range:fullRange]; NSMutableArray *numbers = [NSMutableArray array]; for (NSTextCheckingResult *checkingResult in matches) { NSRange range = [checkingResult range]; NSString *numberStr = [self substringWithRange:range]; NSNumber *number = @([numberStr integerValue]); [numbers addObject:number]; } return numbers; } 


Change the offset of the arguments to a new value, taking into account the added argument
 - (NSArray *)changeByteSignature:(NSArray *)byteSignature { NSInteger value = sizeof(void *); NSMutableArray *marray = [NSMutableArray array]; for (NSNumber *number in byteSignature) { NSInteger offset = [number integerValue] + value; [marray addObject:@(offset)]; } [marray insertObject:@0 atIndex:1]; return marray; } 



Well, in the end we create a new signature using the new format string and NSArray with a new offset. Thus, when invoking the implementation, the owner will be passed according to the documentation as the first argument, shifted due to interception and the original block is called.


The full code is here. It was just an experiment, I had no desire to write this code for use in projects. But I am glad that I was able to complete this business successfully. I'm also glad that maybe I could help someone by putting a solution for generating a string using NSArray on SO.
I hope I managed to bring the material in an understandable form and break into blocks.

Source: https://habr.com/ru/post/276599/


All Articles