Dynamic method replacement

It's probably not the long-term solution we want for registration, i.e. cases where you have many implementers of a common interface. It might still be the right solution for simple snap-together libraries and upward dependencies. (I'm actually running into a version of this now with the CoreLocation and MapKit overlays, though dynamic would probably be overkill to solve it.)

1 Like

I'm not 100% sure I agree with the specifics on this pitch, but I can offer that the Android team I work with unit tests dramatically more effectively with Kotlin/Mockito than our iOS team can with Swift. Our best option so far has been to use Sourcery to auto-gen the protocols and mock objects we want to use in our app & unit tests, but that has it's limits. For example, there's no way to mock a singleton object the way the Android team can. I would love to see a path forward that would address our testing pain points.

2 Likes

Far better to design for testability than rely on dynamic runtime manipulation to put yourself in the middle of a fixed system. After a few years of Kiwi tests in Objective-C, dynamically mocking the world, I’ve found simpler tests to be easier to reason about, faster, and more reliable.

6 Likes

As I understand in proposal it is refers to «client» code. But if we do similar concept for «framework» code? Even may be we could expand this capabilities to make elements of Design by Contract available in Swift. It would be very positive if we had capabilities to check class/entity invariant before and after replaced method execution. For example check that all class invariant properties has correct values after replaced method execution. It leads us to more controller «client» code.
That is not only «client» can inject code before and after original implementation but «framework» code can be injected before and after client code also (ex to check that everything is correct, inject asserts etc). Now we can make only part of that with make function generic but it is very limited to static type cheks. May be also «framework» also optionaly could require to call original implementation as in case when subclass method require to call superclass implementation. All this make really possible to avoid subclassing/inheritance (we make class final and export rules to replace methods in fully controlled way).

I see that it is very unclear how to make this without serious syntax complexity. May be this is really not part of this proposal and it is better to discuss it sometime later. But it is good moment to think about it.

Ok, it sounds like you have the right behavior, but why quotes? Why not just:

extension Something {
   @dynamicReplacement(for: init(x:))

-Chris

2 Likes

No good reason beyond I followed the syntax of @available(swift, obsoleted: 3, renamed: "Struct1.oldApiNoteMethod()")

I see. I'd recommend not following this precedent. I don't recall the full rationale for using string, but I suspect it was just brought over without enough consideration from Clang. Another reason might have been because we wanted to include default parameters and stuff like that, which makes sense in a string form (you can put <##> style tokens in and play other games).

In contrast, in this case, you really are talking about something that goes through name lookup and is resolve and bound.

3 Likes

Okay. Makes sense to me.

+1 to getting rid of quotes. It feels more native, just like #selector(foo(x:)).

However, I don't think there's a precedent for prepositions as argument labels in an attribute.

Here's an alternative, though gerund phrases in attributes have no precedents either.

@dynamicallyReplacing(init(x:))

Or, better, imperative phrases. This has precedents in modifiers such as override.

@dynamicallyReplace(init(x:))
3 Likes

Updated pitch: I expanded the motivation section to include uses cases brought up on this thread so far. I added a sentence about controlling priority based replacement with an environment variable and falling back to an ordering based on load time ordering if this is not set. I added a paragraph pointing out that the declarative syntax for replacement does not preclude adding an expression syntax in the future.

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 currently 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.

dynamic can be used in situations when it is desired to change behavior of an application/framework after its initial compilation. It allows for replacement of a method during load/runtime.

Use cases are:

  • Customizing functions: Additional code to be executed before, after, or instead of the initial implementation. Some examples are logging, mock testing, and patching code.

    • A family of libraries that work well together: library Base exposes a dynamic entry point, and library Advanced adds some additional behavior.
    • Dynamically synthesized wrapper code, similar KVO. Although, this proposal does not propose syntax for changing functions dynamically at runtime (vs load time), the runtime machinery allows for a future expression syntax to replace functions at runtime.
    • Mock testing for when dependency injection and protocols are considered too involved. One could imagine adding a mode similar to -enable-testing where all functions are dynamic.
  • Type safe way of importing new functionality at runtime for example in web services or video games.
    Example:
    The DatabaseAuthenticator module can be loaded at runtime to change the method of authentication while the web application is running.

// Module BasicWebApp

protocol Authenticator {
  func authenticate(user: User) -> Credentials 
}


struct DefaultAuthenticator : Authenticator {
    func authenticate(user: User) -> Credentials  { … }
}


struct WebApplication {
  static dynamic func getAuthenticator() -> Authenticator { 
    return DefaultAuthenticator() 
  }

  func run() {
     request.authenticate(getAuthenticator())
   }
}
// Module DatabaseAuthenticator
struct DatabaseAuthenticator : Authenticator {
    func authenticate(user: User) -> Credentials  { … }
}

extension WebApplication {
  @dynamicReplacement(for: “getAuthenticator()”)
  static dynamic func getDatabaseAuthenticator() -> Authenticator { 
    return DabaseAuthenticator() 
  }
}

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 -- including generic signature and constraints -- 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
}

// Module A
class MyThing<T> {
  static dynamic func doSomething() -> Int { return -1 }
}

// Module B
extension MyThing where T: MyProtocol {
  @dynamicReplacement(for: doSomething)
  func doSomethingVariant1() -> Int {  return 42 } / ERROR: signature of doSomethingVariant1 doesn't match signature of doSomething
}

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
}

This behavior could be controlled by an environment variable SWIFT_USE_PRIORITY_BASED_REPLACEMENT, if set to a non-zero value enables priority base replacement. If not set the last object loaded determines the replacement.

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.

In order to support replacing generic functions on non-``@objc` types the programmer has to add a protocol with the generic function. This is quite a bit of boiler plate code to write but also has a code size overhead for functionality that is not really required (protocol conformances, value witnesses, etc).

protocol Behavior {
  func transform<T, S>(_ x:T, _ theSelf: Something<S>) -> T
}

struct DefaultBehavior : Behavior {
  func transform<T, S>(_ x:T, _ theSelf: Something<S>) -> T { return x }
}

struct Something<S> {
   func transform<T>(_ x: T) -> T {
     return Something_transform_implementation.transform(x, self)
   }
}

var Something_transform_implementation : Behavior = DefaultBehavior()

func Something_transform_replaceImplementation(with newImpl: Behavior) {
  Something_transform_implementation = newImpl
}

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.

Function replacement at program start or loading of a shared library is sufficient to cover many 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 in many use cases. If she wishes to perform the replacement conditionally this can be controlled by loading or not loading a shared library containing the replacements.

The declarative syntax and its runtime implementation does not preclude adding support for an expression syntax to replace functions dynamically in the future.

1 Like

Do you think it would be better to name this load/link-time method replacement then? A name like "dynamic method replacement" is a bit misleading IMO, since it's not really "dynamic" in the sense most people are probably used to thinking about. Which in my mind would leave me to believe I can just hot swamp functions at will.

About the actual proposal content:

What are the performance consequences of this change? Should library authors be wary about marking something dynamic? Is this mostly a cost for program startup/linking time or is the cost also present during calls to replaced functions? How can they reason about the performance if:

  1. The function isn't replaced during load/link
  2. The function is replaced once during load/link
  3. The function has multiple possible replacements

The cost of a call to a dynamic function (non @objc) is going the indirection through a global function pointer which modern processors can predict well. The main cost derives from the semantics of dynamic: the compiler is not allowed to optimize calls to dynamic functions such as inline/specialize the function since the function could be replaced. One cannot make predictions about this slow down as it depends on which optimizations are enabled.

The cost of replacing the function is payed at load time, which amounts to setting the global function pointer to the desired implementation.

2 Likes

Another possible use for this that I haven't heard anyone mention is for dependency injection. I believe Dagger uses this type of behavior to inject objects so they don't need to be passed through at initialization.

Is that something we really miss, though? I have always found this injection approach to be too magical. Judging by a recent thread about DI framework, it appears that fancier DI support is not a thing people would miss that much. (I don’t want to hijack this thread with discussion about DI, just trying to assess if this is a real shortcoming that needs fixing.)

3 Likes

This is a general comment, and not necessarily reflective of anything in this particular proposal, but...

I'm really skeptical of baking dynamism like this into the language. I've made a bit of a name for myself because of my manipulations of the Objective-C runtime, but even I acknowledge that the number of times when you actually need to ship these sorts of dynamic features is exceptionally rare. I'd want to be convinced beyond a shadow of a doubt that there was no other way to implement a large and high-priority feature in order to be supportive of this proposal.

10 Likes

In addition to this and speaking generally, I'd love to see an end to mischief third party SDKs use to "automatically" track the user or other things for you, like view controller presentation, push notification ingestion etc. I want control over the security of my code: the specifics of what data those SDKs consume without refusing to use the SDK altogether, I want to avoid conflicting exchanges and a say over the injection of crash prone code.

8 Likes

I would suggest, if you can, to skip those SDK’s or to turn off those features instead of turning down this pitch.

I know that’s lot of library authors and backend developers have valid reasons to like statically types languages and reject dynamic ones, but on the app/front end side more dynamic languages have been and are still at an advantage due to how easy it is to prototype and iterate on it. That is code that changes often and there is a lot of value in getting it running quickly.

I have never seen many statically (and mostly strongly) typed languages thriving in front end code... in the cases they did was it despite or because of their nature?

1 Like

I have been a frontend developer for all my professional career and I can’t get enough static typing. Also, the industry seems to be clearly moving in the direction of static type checking everywhere, see the boom of type annotations in dynamic languages.

I suggest Playground Driven Development by Point Free as a counter example. Developing with playgrounds is a fantastic experience (modulo Xcode bugs), so it seems that static typing does not prevent rapid iteration. (Also see Haskell or Elm.)

PS. If we were to continue this topic, perhaps let’s create a separate thread?

1 Like

That may be true, but imho the proposal wouldn't turn Swift into a different language and suddenly make prototyping easier, and I don't think that is its goal:
When I'm writing a prototype, I wouldn't annotate a method and write a replacement - I would just change the method.
I consider the type checker to be the reason for potential slowdown compared to more dynamic language which allow you to call method "foo" on an object without needing a "HasFoo" protocol or other guarantees enforced by the compiler.

1 Like

So far, all of the examples could more-easily be expressed by making the dynamic functions mutable closure properties. The only practical differences between them right now are:

  • Closure properties cannot witness a protocol's method requirements (we should fix that)
  • Generic types cannot have static stored properties (closure-type or not - might also be worth supporting, if possible).

Which leads me to another question... what happens to dynamic replacements which are defined in constrained extensions? Do they honour those constraints?

// ModuleA
class MyThing<T> {
  static dynamic func doSomething() -> Int { return -1 }
}

// ModuleB
extension MyThing where T: MyProtocol {
  @dynamicReplacement(for: doSomething)
  func doSomethingVariant1() -> Int { 
    return 42
  }
}
extension MyThing where T == String {
  @dynamicReplacement(for: doSomething)
  func doSomethingVariant2() -> Int { 
    return 1999
  }
}

MyThing<String>.doSomething() // returns ?
MyThing<Int>.doSomething() // returns ?

extension String: MyProtocol {} // What now?
2 Likes