A lot of the discussion around the final/sealed-by-default issue focused on the 
ability in ObjC to extend frameworks or fix bugs in unforeseen ways. Framework 
developers aren't perfect, and being able to patch a broken framework method 
can be the difference between shipping and not. On the other hand, these 
patches become compatibility liabilities for libraries, which have to contend 
not only with preserving their own designed interface but all the undesigned 
interactions with shipping apps based on those libraries. The Objective-C model 
of monkey-patchable-everything has problems, but so does the buttoned-down 
everything-is-static C++ world many of us rightly fear. However, with the work 
we're putting into Swift for resilience and strong versioning support, I think 
we're in a good position to try to find a reasonable compromise. I'd like to 
sketch out a rough idea of how that might look. Public interfaces fundamentally 
correspond to one or more dynamic library symbols; the same resilience that 
lets a new framework version interact with older apps gives us an opportunity 
to patch resilient interfaces at process load time. We could embrace this by 
allowing applications to provide `@patch` implementations overriding imported 
non-fragile public APIs at specific versions:

import Foundation

extension NSFoo {
  @patch(OSX 10.22, iOS 17)
  func foo() { ... }
}

By tying the patch to a specific framework version, we lessen the compatibility 
liability for the framework; it's clear that, in most cases, the app developer 
is responsible for testing their app with new framework versions to see if 
their patch is still needed with each new version. Of course, that's not always 
possible—If the framework developer determines during compatibility testing 
that their new version breaks a must-not-break app, and they aren't able to 
adopt a fix on their end for whatever reason (it breaks other apps, or the 
app's patch is flawed), the framework could declare that their new version 
accepts patches for other framework versions too:

// in Foundation, OSX 10.23
public class NSFoo {
  // Compatibility: AwesomeApp patched the 10.22 version of NSFoo.foo.
  // However, RadicalApp and BodaciousApp rely on the unpatched 10.22 behavior, 
so
  // we can't change it.
  @accepts_patch_from(AwesomeApp, OSX 10.22)
  public func foo() { ... }
}

A sufficiently smart dynamic linker could perhaps resolve these patches at 
process load time (and probably summarily reject patches for dylibs loaded 
dynamically with dlopen), avoiding some of the security issues with arbitrary 
runtime patching. For public entry points to be effectively patchable, we'd 
have to also avoid any interprocedural optimization of the implementations 
within the originating module, so there is a performance cost to allowing this 
patching by default. Sufficiently mature (or arrogant) interfaces could perhaps 
declare themselves "unpatchable" to admit IPO within their own module. (Note 
that 'fragile' interfaces which admit cross-module inlining would inherently be 
unpatchable, and those are likely to be the most performance-sensitive 
interfaces to begin with.)

-Joe
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to