Dynamic method replacement


(Arnold Schwaighofer) #1

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.


Allow dynamic keyword on non-objc properties
(John McCall) #2

I think the key question is what the requirements on controlling replacement actually are. If people really only want to perform replacement once, at load time, that's one thing. If they want to be able to dynamically enable or disable a replacement, that's different, and it may influence the right way to surface this in the language.

Also, there's a secondary question about whether it's useful to be able to delegate to the replaced implementation.

So in general, I think what we really need from this pitch phase is some good ol' requirements-gathering.


(Arnold Schwaighofer) #3

This is a good point. The pitch proposes:

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
}

(Jonathan Hise Kaldma) #4

What problem does this solve?


(Arnold Schwaighofer) #5

What problem does this solve?

Type safely replacing/extending the implementation of a method across modules without the need to recompile the module containing the original method.


(John McCall) #6

Oh, sorry, I missed that.


(Joe Groff) #7

What Arnold is suggesting sounds like it might be a more limited form of an idea I had a few years back:

Among other possible use cases, a feature like this could be adopted by framework developers who wanted to allow limited patching of their behavior by clients for bug fixing or compatibility purposes, which is something app developers have historically done a lot with Objective-C frameworks.


(Jonathan Hise Kaldma) #8

Thanks Joe! That answers the question. But wouldn’t this pitch require that the framework developer anticipate their own bugs and mark their method/properties dynamic? And if you can anticipate the bugs, well... why not just fix them instead?


(Jonathan Hise Kaldma) #9

I’ll echo @dabrahams from the thread Joe linked: Can you provide a code example showing how this would be used to solve a problem in a real framework? You say something similar is possible in Objective-C. How is that used in Objective-C frameworks, e.g. in Cocoa?


(John Scott) #10

Thanks for adding a pointer to this from my recent pitch Allow dynamic keyword on non-objc properties. Adding the inverse link :slight_smile:.


(Dave Abrahams) #11

Isn't that what resilience is for? I realize that this proposal is addressing a different problem, but I guess I don't really understand it. As described, it sounds exactly like what we'll do with resilience.


(Alexey Kravchenko) #12

Hello

It is important that method swizzling (also as isa swizzling) in ObjC is not part of language syntax - it is part of ObjC runtime (in C language). It is even was not documented as «method swizzling» feature by Apple in its own documentation. This ability is only side effect of ObjC message-forwarding idea implementation. It is always was some sort of «hacks» in every-day code and even sometimes strongly banned by code conventions. Yes it is can be helpful in some sort of self-written frameworks with rich introspection and dynamism (like Core Data) but in this case ObjC runtime is not the most complex part of your code and it is no so often really needed. Also it is sometimes used in client code to «hack» some system framework behaviours or «bugs». In hands of unexperienced developers (whom most among all) it usually leads to undefined behaviours and hard to fix bugs in own code. This ability is some sort of old pos as function class_poseAs ([MyClass class], [SystemClass class]) which gave as ability to replace class implementation in runtime - it was blocked by Apple many years ago, there was some regrets about it but as we see now it is was not wrong decision.

Should we have method swizzling in pure Swift? I’m not sure.

First of all i think that we need alternative for ObjC KVC/KVO in pure Swift. And Swift KeyPaths, Dynamic member lookup and another dynamic features is first big step to that. May be method swizzling could become more safety to implement and use after that, may be not. As I see it is too early to say clearly.


(Rod Brown) #13

I think ultimately this comes back to a discussion that we’ve circled around a lot in the dynamic discussion in Swift:

Is a library developer better placed to enforce rules, or should an end user have the capability to override this behaviour?

This is as much an ideological discussion as it is a technical one. And people will sit on both sides of the argument. Much of Objective-C’s claim to fame was the dynamism that allowed a lot of flexibility, but was often called the Wild West of development. Conservative people were not very supportive of this.

I think ultimately the question comes down to putting the shoe on the other foot: if you’re a framework author, what is the impact to you if someone injects and overrides things you’ve defined without understanding them? And if you’re an end user application, what is the impact of libraries that you cannot override when there are behaviours you need to adjust due to bugs or other issues?

From my perspective, the second case is more frustrating. If you’re a library developer, it doesn’t directly affect you. You’re not the one taking the risk things will crash. But if you’re an app developer, you’re the one unable to fix a problem. You’re also the one who’s unable to take a risk fixing this because someone else decided “you won’t need this functionality”.

I feel that the views on this for many years have been skewed toward library developer perspectives, and I say this as a library developer myself. I personally support this functionality being added.


(Alexey Kravchenko) #14

I understand your point, but it is true if we can fix problem via method swizzling. There are a lot of private framework code which can not be fixed without access to source code in any case. Method swizzling can solve only some of obvious problems (crash or exception). Another point is that method swizzling can add ability to malicious your framework code. Or it can break license agreement of framework usage. I have no really answer how we should make it right.

In any case now we have this ability via ObjC runtime. It is good point that we should think about pure Swift alternatives to such cases because once upon a time Swift throws out ObjC runtime or new frameworks will be pure Swift-only.


(Alexey Kravchenko) #15

I think you are right. It is look like we gradually moving to application resilience problem. Until that time we can fix third-party framework «crash» via method swizzling but Swift is becoming more and more popular language. In Swift 5 we should get ABI stability and pure Swift framework will be delivered to clients in binary form more and more often (now most of that is sources). Very, very interesting problem.


(John Scott) #16

A solution which respects the current language and requirement constraints would look something like this:

class Thing {
    public func theAnswer() -> Int {
        if let replaceableAnswer = Thing.replaceableAnswer {
            return replaceableAnswer(self, self.theAnswerOriginal)
        }
        else
        {
            return theAnswerOriginal()
        }
    }
    
    private func theAnswerOriginal() -> Int {
        return 20
    }
    
    static var replaceableAnswer: ((_ thing: Thing, _ original: () -> Int) -> Int)?
}

let thing = Thing()

print(thing.theAnswer())

Thing.replaceableAnswer = {
    (thing, original) in
    return original() + 22
}

print(thing.theAnswer())

I think this shows just how Allow dynamic keyword on non-objc properties and this pitch is different.

It appears that dynamic has alight been slightly repurposed in Swift by the creation of @dynamicMemberLookup. Maybe replaceable would be a more accurate keyword for this.


(Karl) #18

Method swizzling isn't just useful for patching bugs - it's also useful to install hooks. For example, I recently worked on an App which included its own styling/theming system. To make this work, I had to install a hook in UIView so that all instances (including instances of system-provided subclasses like UITableView) automatically pulled their styling attributes from the hierarchy and applied them.

Otherwise, I would have had to subclass every kind of view which was used in the application.


(Arnold Schwaighofer) #19

This is different to resilience in that resilience requires recompilation of the module containing the implementation to change an implementation.

dynamic as proposed here allows for methods to be replaced at runtime. Only the replacement method needs to be recompiled.


(Jordan Rose) #20

You all know I'm very much on the side of "library author should be in control" (cf. past support for the open/public distinction). For that reason, I'm strongly, strongly against any default replaceability. Given that, what is this proposal for? "When would a library author use this?" is a very good question. And I can think of a handful of answers:

  • A family of libraries that work well together: library Base exposes a dynamic entry point, and library Advanced adds some additional behavior. (This could be from the same vendor, or just an open extension point of some kind. It's probably not the long-term solution we want for registration, but you could implement that.)

  • Dynamically synthesized wrapper code, like KVO. The proposal Arnold's written here doesn't have syntax for that, but it probably has all the run-time underpinnings unless the runtime assumes that the implementation will never change after the first use. I do think it's probably an important use case, though, and even if we don't provide it now we should keep it in mind.

  • Testing, potentially. I'm not sure how I feel about this one, but people have been asking for simpler mock objects than "just do everything in terms of a protocol", and this might be one way. However, we probably wouldn't want to use the normal dynamic modifier for this; either we'd want something special or we'd want to have -enable-testing make everything dynamic. I'm not sure of the performance implications of that.

I'm also wary of anything that requires static initializers to do correctly, since Swift has been trying very hard to get away from them. But it sounds like we have ideas there.

I'm not sure why this is limited to members of types. Any reason why top-level functions and computed properties can't be dynamic?

One last thing: I don't like integer priorities; they run into the "what if two people did this" problem. IM(H?)O we should just say "if two libraries do this, both replacements are applied one on top of another; if library A links to library B, library B's replacement will be the 'inner' one". (Or the other way around. That can be a sub-discussion.)


(Arnold Schwaighofer) #21

This is just an unintended omission in the text. dynamic should work on top-level functions/computed properties.