preface

Open source address:MessageMock

When we debug code or write unit tests, we often need to go through a series of pre-actions or directly modify source code data in order to trigger a particular scenario. OCMock is well known for solving these problems. However, it does not support multithreading, has weird interfaces, makes repeated calls, and has complex type handling. After looking at the source code, I decided to take a different approach. Method “emulation” and “validation” based on objc_msgSend.

MessageMock hits the target method with any call to [target selector] :

  • Modify the return value and parameters of the target method
  • Verify the target method return values and parameters
  • Skip the target method call
  • Gets the hit count of the target method

Core principles

Use Fishhook to point the pointer to objc_msgSend to the custom function to implement Hook, insert two cuts before and after the original function call, Hook operation can refer to The processing code of Teacher Deming.

Once you get the cut, you can intercept all objective-C method calls, ready to do anything “bad.” However, it is worth noting that MessageMock code must not contain any Objective-C method calls in its path, otherwise it will loop forever, so the source code is mostly implemented using C++ / Assembly.

Modify and check the return values

Currently we only consider returns of less than or equal to pointer types, which are d0 and x0.

Change the return value immediately after the origin_msgSend call:

. Bl origin_msgSend Cache x0-x8 q0-q8 value BL after_msgSend // Save the value to be changed to x2... STR x2, [sp, #160] STR x2, [sp] // change the position of d0 on the stack... X0 -x8 q0-q8Copy the code

To check the return value, simply call the external function pointer in after_msgSend.

Modify and check parameters

For now, only parameters of pointer type less than or equal to are considered. We roughly test the register-only case of method calls:

General register parameters up to 6 (X2-X7) Float register parameters up to 8 (D0-D7 compiler limit cannot exceed 6 in a row)Copy the code

The allocation of parameters to registers is relatively simple, that is, X2-x7 / d0-d7 next to each other, until used up.

Modifying method inputs is handled just before the origin_msgSend call:

Cache x0-x8 q0-q8 value bl before_msgSend // Write the data that needs to be modified to x2-x7 / d0-d7... str d0, [sp, #0] str d1, [sp, #16] ... Up to d7 // modify the stack d0-D7 corresponding position of data... str x2, [sp, #176] str x3, [sp, #184] ... Until x7 // modifies the x2-x7 position on the stack... X0-x8 q0-q8; // origin_msgSend; // origin_msgSend;Copy the code

The check callback for the before_msgSend function is simply called next to the function pointer passed in externally.

Skip the original method call

The treatment is simple:

. b 1f ... bl origin_msgSend ... 1:...Copy the code

Just note that if you skip the original method call, modifying the input parameter will not work.

Problems with destructions

Some embedded assembly is used in the code, because the destructor will be triggered at the end of the scope, it may affect the assembly code at the end of the target function, resulting in register state changes and thus cause Crash, most use {… } limiting the scope solves this problem.

Data security

The underlying design uses a C++ class to configure various processes:

class MethodMatcher { public: ... Long reference = 0; // Long reference = 0; Long using_count = 0; /// uintptr_t target = 0; Uintptr_t selector = 0; . };Copy the code

A mapping table is used to store all configuration data:

typedef std::unordered_map<uintptr_t, std::unordered_map<uintptr_t, MethodMatcher *>> MethodMatcherMap;
static MethodMatcherMap *matcher_map = NULL;
Copy the code

Root of the problem

MessageMock has two important abilities to modify return values and input parameters. When these custom return values are Objective-C objects, the code is handled directly by assembly instructions. If the compiler fails to insert retain in the right place, these Objective-C objects may be freed prematurely (such as the end of the current scope).

When the return value and input parameter of a custom method are Objective-C objects, they are called free objects for ease of understanding.

The life cycle of a floating object

For floating objects, the target reference count is currently incremented by __bridge_retained. Since these objects are attached to MethodMatcher *, these Objective-C objects whose reference count is incremented by one are not freed, so MethodMatcher * is not freed.

Once a floating object is used by a method, it is best to continue releasing it until the origin_msgSend method is called.

Critical region consideration

The first thought might be to protect the entire before_msgSend/origin_msgSend/after_msgSend code as a critical section, which is definitely not appropriate. For this problem, read and write security “markers” can be used to minimize critical sections.

The tags here are using_count and reference.

So when will it be released? The right time is after the origin_msgSend call is complete. So if you use a floating object in MethodMatcher * before the origin_msgSend call, the using_count property is ++, and if you use a floating object in MethodMatcher * after the origin_msgSend call, Its using_count attribute is just –.

Upper level usage considerations

MethodMatcher * is released and crashes if a scope is not finished, since the upper interface is running in objective-C:

@implementation MessageMocker { MethodMatcher *_matcher; . } init { _matcher = new MethodMatcher(); ++_matcher->reference; . } dealloc { --_matcher->reference; // Try to remove matcher}Copy the code

Therefore, from the perspective of concurrent data security, the release of a matcher needs to satisfy: reference == 0 &&using_count == 0.

Interface design

The use of chained syntax was not the original intention of the author, but was based on some special considerations.

The generic types we usually deal with are actually id types. It is difficult to implement true generics by conventional means, such as modifying the return value of the interface has many:

- (void)mockReturnObject:(id)value; - (void)mockReturnInt:(int)value; - (void)mockReturnFloat:(float)value; .Copy the code

Considering the simplicity of the interface and implementation, we still want to make a true generic interface, preferably one that can support the compiler’s index. Two things come to mind: C multiarguments and macros.

Using C multiparameter implementation:

- (void)mockReturn:(const char *)type, ... ;Copy the code

The user still needs to pass the type parameter, which is very awkward, and would like something like this:

- (void)mockReturn:... ;Copy the code

Macros are called in the same way as macro(ARG). Macros can be used to simplify arguments:

#define mockReturn(arg) mockReturn:@encode(typeof(arg)), arg, nil
Copy the code

But the compiler does not index this macro, so it is improved:

@property (nonatomic, copy, readonly) MessageMocker *(^mockReturn)(const char *type, ...) ; #define mockReturn(arg) mockReturn(@encode(typeof(arg)), arg, nil)Copy the code

Once this is done, the macro can be indexed (you can interview it in the code), simplifying the parameters.

Other interfaces are also conveniently made into chain calls, which are also more elegant to use. Here is a simple example:

// Skip NSObject's new method call and return nil messagemocker.build (nsobject. self, @selector(new)).mockReturn(nil).start(); // Always return nil when used id res = [NSObject new]; //res == nilCopy the code

After the language

Considering that there are not many scenarios in which this code can be implemented, it needs to support at least x86 machines and data processing larger than pointer types before it can replace OCMock. Considering the time cost, I have made a prototype so far for everyone’s amusement. In addition, the source code of C++ / Assembly is not professional, performance and design is not optimal, hope the leaders of the advice would be grateful 😁.