Transferred from the public account “let technology a melon public food”, the bottom of the article scan code attention.

primers

With the launch of SwiftUI at WWDC in June, it feels like Swift has become a hot topic. After the conference, there were several tweets discussing a new feature called @_dynamicreplacement (for:).

So I searched for the corresponding keyword in the Swift community and saw a post on Dynamic Method Replacement **. ** After climbing several floors, we can see how to use the Playground (macOS 10.14.5, Swift 5.0). Note that the following Demo can only be run in the project. The Playground will error: Couldn’t lookup symbols: Error).

class Test {
    dynamic func foo(a) {
        print("bar")}}extension Test {
    @_dynamicReplacement(for: foo())
    func foo_new(a) {
        print("bar new")}}Test().foo() // bar new
Copy the code

Isn’t this a light spot to see? The long-awaited return of Method Swizzling?

It was a surprise at first, but hook logic is rarely used in personal development (not corporate projects, of course). Until one day, a friend encountered a problem, and then did a more in-depth study of this thing….

Method Swizzling in Objective-C

First let’s write a scene from Method Swizzling in ObjC:

//
// PersonObj.m
// MethodSwizzlingDemo
//
// Created by Harry Duan on 2019/7/26.
// Copyright © 2019 Harry Duan. All rights reserved.
//
    
#import "PersonObj.h"
#import <objc/runtime.h>
    
@implementation PersonObj
    
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL oriSelector = @selector(sayWords);
        SEL swiSelector = @selector(sayWordsB);
        Method oriMethod = class_getInstanceMethod(class, oriSelector);
        Method swiMethod = class_getInstanceMethod(class, swiSelector);
        method_exchangeImplementations(oriMethod, swiMethod);
        
        SEL swi2Selector = @selector(sayWorkdsC);
        Method swi2Method = class_getInstanceMethod(class, swi2Selector);
        method_exchangeImplementations(oriMethod, swi2Method);
    });
}
    
- (void)sayWords {
    NSLog(@"A");
}
    
- (void)sayWordsB {
    NSLog(@"B");
    [self sayWordsB];
}
    
- (void)sayWorkdsC {
    NSLog(@"C");
    [self sayWorkdsC];
}
    
@end
Copy the code

Void sayWords (); void sayWords (); void Swizzling ();

At execution, we call the -saywords method:

PersonObj *p = [PersonObj new];
[p sayWords];
    
// log
201907 -26 - 16:04:49.231045+0800 MethodSwizzlingDemo[9859:689451] C
201907 -26 - 16:04:49.231150+0800 MethodSwizzlingDemo[9859:689451] B
201907 -26 - 16:04:49.231250+0800 MethodSwizzlingDemo[9859:689451] A
Copy the code

As expected, the result outputs CBA because the -saywords method is first replaced with -saywordsb, whose replacement result is replaced with -saywordsc. Since Swizze’s methods all call the original method, it will output CBA.

To review how Method Swizzling works in Runtime, we can summarize it in one sentence: swapping Method Pointers. ObjC Runtime 750 ObjC Runtime 750

void method_exchangeImplementations(Method m1, Method m2)
{
    if(! m1 || ! m2)return;
    
    mutex_locker_t lock(runtimeLock);
		
		// the imp pointer is swapped between m1 and m2
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;
    
    
    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?
    
    flushCaches(nil);
		
	// Update the RR/AWZ flags information for each method
	// RR/AWZ = Retain Release/Allow With Zone
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}
Copy the code

Method Swizzling is implemented as long as we can access the Method instance specified by ObjC, modify the imp pointer corresponding to the pointer, and then update the reference count and memory equal to the Class related information.

A serial Hook scene

The output ABC scenario above was encountered by my friend. When creating a development toolchain that dynamically loads plug-ins based on a dynamic library, the main project opens up interfaces that emulate Ruby’s alias_method notation so that you can extend the implementation by injecting custom implementations into the underlying methods. Of course, this ability to expose the scheme is not very good, just one of the most crude plug-in scheme implementation method.

Of course we’re not talking about ObjC today, because ObjC is predictable in terms of the Runtime mechanism. If we use the Dynamic Method Replacement scheme in Swift 5.0 to implement this scenario in the Swift project.

import UIKit
    
class Person {
    dynamic func sayWords(a) {
        print("A")}}extension Person {
    @_dynamicReplacement(for: sayWords())
    func sayWordsB(a) {
        print("B")
        sayWords()
    }
}
    
extension Person {
    @_dynamicReplacement(for: sayWords())
    func sayWordsC(a) {
        print("C")
        sayWords()
    }
}
    
class ViewController: UIViewController {
    
    override func viewDidLoad(a) {
        super.viewDidLoad()
        Person().sayWords()
    }
}
Copy the code

Visually, we complete the implementation of Method Swizzling by explicitly declaring Swift Functions. Run the code and find that the result is not as good as we expected:

C
A
Copy the code

Why do the results show only two? I switched the order of the two extensions to try again, and the printing result changed to BA again. So you can kind of summarize the rule, in order of execution, the last declaration will take effect. So how to implement this kind of serial Hook scenario? I didn’t think of anything at the code level.

Guess from Swift source code@_dynamicReplacementimplementation

According to the logic of normal programmers, if we are reconstructing the code of a module, the new module code should be better than the previous way and cover all the previous logic scenarios in terms of function and efficiency. If Swift supports this kind of serial modification scenario, then this new Feature release is actually incomplete! Therefore, we started to look through the PR code of Swift Feature to explore the principle of Dynamic Method Replacement.

The Dynamic Method Replacement feature is called issue-20333, and the author has posted two interesting codes:

/ / / pieces
// Module A
struct Foo {
 dynamic func bar(a){}}// Module B
extension Foo {
  @_dynamicReplacement(for: bar())
  func barReplacement(a){...// Calls previously active implementation of bar()
    bar()
  }
}
    
/ / / segment 2
dynamic_replacement_scope AGroupOfReplacements {
   extension Foo {
     func replacedFunc(a){}}extension AnotherType {
     func replacedFunc(a){}}}AGroupOfReplacements.enable()
...
AGroupOfReplacements.disable()
Copy the code

He wanted the dynamic replacement feature to be compatible with dynamic replacement and enable switching, with some keyword tags and internal tags. Since they have planned enable and disable methods to control enablement, retrieve their implementations.

MetadataLookup. CPP file l1523-L1536: Enable/disable

// Metadata.h#L4390-L4394:
// https://github.com/apple/swift/blob/659c49766be5e5cfa850713f43acc4a86f347fd8/include/swift/ABI/Metadata.h#L4390-L4394
    
/// a list implementation of dynamic replacement functions
/// there is only a next pointer and an IMP pointer
struct DynamicReplacementChainEntry {
  void *implementationFunction;
  DynamicReplacementChainEntry *next;
};
    
// MetadataLookup.cpp#L1523-L1563
// https://github.com/aschwaighofer/swift/blob/fff13330d545b914d069aad0ef9fab2b4456cbdd/stdlib/public/runtime/MetadataLooku p.cpp#L1523-L1563
void DynamicReplacementDescriptor::enableReplacement() const {
  // Get the root node
  auto *chainRoot = const_cast<DynamicReplacementChainEntry *>(
      replacedFunctionKey->root.get());
    
  // Ensure that the method is enabled by iterating through the list
  for(auto *curr = chainRoot; curr ! = nullptr; curr = curr->next) {if (curr == chainEntry.get()) {
	  // If this method is found in the Replacement chain, the operation is enabled and interruptedswift::swift_abortDynamicReplacementEnabling(); }}// Save imp of Root node to current and insert current header
  auto *currentEntry =
      const_cast<DynamicReplacementChainEntry *>(chainEntry.get());
  currentEntry->implementationFunction = chainRoot->implementationFunction;
  currentEntry->next = chainRoot->next;
    
  // Root Continues to insert the head
  chainRoot->next = chainEntry.get(a);// The imp of Root is replaced by replacement
  chainRoot->implementationFunction = replacementFunction.get(a); }// Disable does the same thing
void DynamicReplacementDescriptor::disableReplacement() const {
  const auto *chainRoot = replacedFunctionKey->root.get(a); auto *thisEntry = const_cast<DynamicReplacementChainEntry *>(chainEntry.get());
    
  // Find the entry previous to this one.
  auto *prev = chainRoot;
  while(prev && prev->next ! = thisEntry) prev = prev->next;if(! prev) { swift::swift_abortDynamicReplacementDisabling();return;
  }
    
  // Unlink this entry.
  auto *previous = const_cast<DynamicReplacementChainEntry *>(prev);
  previous->next = thisEntry->next;
  previous->implementationFunction = thisEntry->implementationFunction;
}
Copy the code

We find that for each dynamic method processed in Swift, a Dynamic replacement linked list is established for it to record the implementation record. So that means that no matter how many times we do @_dynamicreplacement to the original dynamic, the implementation will in principle be recorded. However, I have not found the corresponding logic of the execution code after calling the method, so I cannot judge what Swift did at the time of calling.

Solve problems with Unit Test

The following idea was provided by my friend @Whirlwind. Since we can’t find the implementation of the call, take a different approach: Since Swift already chain-logs all implementations, there should be this logic test at unit test time.

After searching a number of unit test files based on keywords and file suffixes, we found this file dynamic_replacement_chaining. Swift. We note the execution command of L13:

// RUN: %target-build-swift-dylib(%t/%target-library-name(B)) -I%t -L%t -lA %target-rpath(%t) -module-name B -emit-module -emit-module-path %t -swift-version 5 %S/Inputs/dynamic_replacement_chaining_B.swift -Xfrontend -enable-dynamic-replacement-chaining
Copy the code

-xfrontend -enable-dynamic-replacement-chaining was added to the command parameters. This is like Flags in Build Settings. Compile Flags look at all Compile Flags in Build Settings and try to write them to Other Swift Flags:

Recompile and run, and you find something amazing:

Unexpectedly, we achieved the result we wanted. It shows that our experiment and conjecture are correct. Swift saves all imp implementations when dealing with dynamic, and also has a way to trigger implementation according to the linked list of records.

Some malicious speculation

Although @_dynamicreplacement was brought into Swift in Swift 5, no trace of Release log was found in the official forum and the official warehouse, and I only learned about it orally from my Twitter friends. And while this PR has been going on for a year, Apple secretly merged the Master branch before this year’s WWDC.

** I have to guess that Apple customized the Feature for SwiftUI. ** This can also be seen in other features such as Function Builder (for SwiftUI DSL), Combine (for Dataflow & Binding in SwiftUI). Is it a good thing for the anti-community to customize languages for their own technology products without consent?

Here’s a black question mark, maybe next year’s me will say “Sweet”!