Pitch: Dynamic method replacement
Introduction
Objective-C allows swapping method implementations "swizzling" e.g. via the method_setImplemenation API. We can make use of this facility in Swift by marking methods with @objc dynamic
.
class Thing : NSObject {
@objc dynamic
func original(x: Int) {
print("original called")
}
}
extension Thing {
@objc dynamic
func replacement(x: Int) {
print("replacement called")
}
}
func beforeAfter() {
let thing = Thing()
thing.original(x: 10) // calls original(x:)
let origMethod = #selector(Thing.original(x:))
let replacementMethod = #selector(Thing.replacement(x:))
let method: Method? = class_getInstanceMethod(Thing.self, origMethod)
let swizzleMethod: Method? = class_getInstanceMethod(Thing.self, replacementMethod)
let swizzleImpl = method_getImplementation(swizzleMethod!)
method_setImplementation(method!, swizzleImpl);
thing.original(x: 10) // calls replacement(x:)
}
Downsides of using this approach in Swift are:
- It's very hard to use, there is zero type safety
- If you forget to mark the entity as
dynamic
the swizzling won't take effect, because Swift code won’t necessarily call through the Objective-C entry point - It only works for @objc entities, which limits it to methods of classes that can be expressed in the subset that’s exposed to Objective-C
We propose to extend a similar functionality to all Swift classes, structs, and enums. To allow the runtime to replace a method's, property's, initializer's, or subscript's implementation mark it with dynamic
.
struct Thing {
dynamic var someNumber : Int {
return 10
}
}
To replace the implementation write a method of the same type inside the same scope and mark the method with the @dynamicReplacement(for:)
attribute specifying the original method.
extension Thing {
@dynamicReplacement(for: "someNumber")
var newNumber : Int {
return 42
}
}
The Swift runtime will perform the replacement on application start/loading of the shared library containing the replacement.
Motivation
In Swift the ability to replace a method's implementation is limited to classes using the Objective-C runtime. Replacing a method's implementation can be useful in scenarios where the type is not necessarily an Objective-C class or even a class.
If a developer foresees the necessity of future extension/replacement of a method she has to explicitly add an extension thunk.
struct Thing {
static var extensionThunkNewNumber = { return 10 }
var newNumber : Int {
return extensionThunkNewNumber()
}
}
And add code to perform the replacement.
extension Thing {
static func update() {
extensionThunkNewNumber = { return 42 }
}
}
// somewhere
Thing.update()
In larger projects this can be a lot of boilerplate code to write and maintain and puts the burden on the developer implementing the extensible method. The proposed solution allows a developer to annotate methods with the dynamic
attribute and another developer to replace methods using a declarative syntax: @dynamicReplacement(for:)
. Replacements can be collected in shared libraries that can be loaded on demand to perform the replacement.
Implementation
The compiler no longer restricts the dynamic
attribute to @objc
methods in Swift 5 mode. It is allowed on methods, properties, and subscript in classes, structs, and enums and extensions thereof.
class Object {
dynamic var x : Int
}
extension Object {
dynamic func method() {}
}
enum Discriminator {
case A
case B
dynamic func var y {
return 5
}
}
The @dynamicReplacement(for:)
attributes indicates which dynamic
declaration is replaced by the new declaration. This declaration must have the same type and be on an extension of the owning type or at module scope for dynamic
non-member declarations.
// Module A
extension Set where Element: SomeProtocol {
dynamic func foo() { }
dynamic func bar() { }
}
// Module B
extension Set {
@ dynamicReplacement(for: "foo()")
dynamic func myFoo() { } // ERROR: signature of MyFoo doesn't match signature of foo
}
extension Set where Element: SomeProtocol {
@dynamicReplacement(for: "bar()")
dynamic func myBar() { } // okay: signatures match
}
A call to the original function from within its replacement will call the original function definition---not recurse to itself. For example:
// Module A
dynamic func theAnswer() -> Int {
return 20
}
// Module B
@dynamicReplacement(for: "theAnswer()")
dynamic func myAnswer() {
return theAnswer() + 22
}
Implementation Model
Calling a dynamic
method requires indirection thorough a global variable storing the current method's implementation. Replacing a method is handled by the runtime by assigning a new implementation to that variable on load of the module containing the replacement.
Expressed in Swift this might look like:
// Module A
public func theAnswerOriginal() -> Int {
return 20
}
public var theAnswer: () -> Int = theAnswerOriginal
// Module B
func myAnswer() {
return theAnswerOriginal() + 22
}
// Somehow executed to set the theAnswer value to the replacement.
theAnswer = myAnswer
The assignment of the replacement function could be executed as a static initializer on startup or when a shared library is loaded. To give the runtime more control we suggest to put the pair of global variable and replacement function in a special section of the metadata that is interpreted by the runtime. The runtime can warn if multiple dynamic replacements for the same function are performed, furthermore a resolution strategy can be implemented using a priority number if multiple such replacements in different shared libraries are encountered.
// Module C
@dynamicReplacement(for: theAnswer(), priority: 3) // the priority for the original function is one, the default priority is two.
dynamic func myAnswer() {
return theAnswer() + 22
}
Alternatives
dynamic
only on @objc declarations
We could keep the existing restriction of dynamic
to @objc
declarations. This would leave Swift platforms that don't support an Objective C runtime without support for replacing method implementations. Furthermore, native Swift classes, structs, and enums would be amiss of the feature even on platforms with Objective C support.
Programatic instead of declarative replacement
The proposal suggests using the @dynamicReplacement(for:)
attribute on declarations to mark functions as replacements. One could imagine an alternative implementation where functions can be dynamically replaced at runtime using runtime functions similar to Objective C's method_exchange.
let original = #method_selector(for: Set<Element>.foo()) where Element: SomeProtocol
let replacement = #method_selector(for: Set<Element>.foo()) where Element: SomeProtocol
let success = swift_method_exchange(original, replacement)
Note that such a syntax would require first class generic function values.
We believe the function replacement at program start or loading of a shared library is sufficient to cover most use cases of dynamic function replacements. The declarative syntax has the advantage that it is easy audible: replacements can be found by looking at function declarations. Specifying constraints on generic types falls out from the extension syntax. The decision when to execute the replacement is a detail/burden that does not need to be exposed to the programmer. If she wishes to perform the replacement conditionally this can be controlled by loading or not loading a shared library containing the replacements.