Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unconventional Swift Patterns

JP Simard
August 11, 2023

Unconventional Swift Patterns

Forget best practices, this talk celebrates Swift programming patterns that break the mould. Should you use them everywhere? Heavens no. Should you occasionally colour outside the lines to give your project superpowers? Yes please!

Talk given on August 11, 2023 at SwiftTO: https://www.swiftconf.to/

JP Simard

August 11, 2023
Tweet

More Decks by JP Simard

Other Decks in Programming

Transcript

  1. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } }
  2. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } // Defined in external library struct ExternalType { init<Value>(_ value: Value) { print("external") } }
  3. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } ExternalType(1) // Prints: override -> trampoline -> external
  4. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } ExternalType(1) // Prints: override -> trampoline -> external 1
  5. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } ExternalType(1) // Prints: override -> trampoline -> external 2
  6. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } ExternalType(1) // Prints: override -> trampoline -> external 3
  7. Generics Trampolines! // Defined in external library struct ExternalType {

    init<Value>(_ value: Value) { print("external") } } extension ExternalType { init<Value: Equatable>(_ value: Value) { print("override") self.init(trampoline: value) } init<Value>(trampoline: Value) { print("trampoline") self.init(trampoline) } } ExternalType(1) // Prints: override -> trampoline -> external
  8. Why do this? • Don’t have to change your call

    sites • Keep using your dependencies as you always have
  9. Why not do this? • It’s really implicit and sneaky

    • Your colleagues might not like you • When the external API changes type signatures you need to update
  10. Protocols Silence deprecation warnings #pragma clang diagnostic push #pragma clang

    diagnostic ignored 
 “-Wdeprecated-declarations" /* ... code using the deprecated API ... */ #pragma clang diagnostic pop
  11. Protocols Silence deprecation warnings struct Person { let name: String

    func attendFilmFestival() { /*...*/ } } let person = Person(name: "Ellie") person.attendFilmFestival()
  12. Protocols Silence deprecation warnings struct Person { let name: String

    @available(*, deprecated) func attendFilmFestival() { /*...*/ } } let person = Person(name: "Ellie") // warning: 'attendFilmFestival()' is deprecated person.attendFilmFestival()
  13. Protocols Silence deprecation warnings protocol FestivalAttending { func attendFilmFestival() }

    extension Person: FestivalAttending {} let person = Person(name: "Ellie") // warning: 'attendFilmFestival()' is deprecated person.attendFilmFestival()
  14. Protocols Silence deprecation warnings protocol FestivalAttending { func attendFilmFestival() }

    extension Person: FestivalAttending {} let person = Person(name: "Ellie") // No warning (person as FestivalAttending).attendFilmFestival()
  15. Why do this? • Keep your codebase warning-free* • Sometimes

    deprecated APIs don’t have equivalent replacements
  16. Why not do this? • Sweeps warnings under the rug

    • Deprecated API might be fully removed in the near future and catch you by surprise
  17. dlsym Access internals of your dependencies void (*swift : :

    swift_task_enqueueGlobal_hook)( Job * job, swift_task_enqueueGlobal_original original) = nullptr;
  18. dlsym Access internals of your dependencies void swift : :

    swift_task_enqueueGlobal(Job * job) { if (swift_task_enqueueGlobal_hook) swift_task_enqueueGlobal_hook(job, swift_task_enqueueGlobalImpl); else swift_task_enqueueGlobalImpl(job); } void (*swift : : swift_task_enqueueGlobal_hook)( Job * job, swift_task_enqueueGlobal_original original) = nullptr;
  19. dlsym Access internals of your dependencies typealias Original = @convention(thin)

    (UnownedJob) - > Void typealias Hook = @convention(thin) (UnownedJob, Original) - > Void let swift_task_enqueueGlobal_hook = dlsym( dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook" ) .assumingMemoryBound(to: Hook ? . self)
  20. dlsym Access internals of your dependencies typealias Original = @convention(thin)

    (UnownedJob) - > Void typealias Hook = @convention(thin) (UnownedJob, Original) - > Void let swift_task_enqueueGlobal_hook = dlsym( dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook" ) .assumingMemoryBound(to: Hook ? . self) swift_task_enqueueGlobal_hook.pointee = { job, original in print("hooked") original(job) }
  21. dlsym Access internals of your dependencies typealias Original = @convention(thin)

    (UnownedJob) - > Void typealias Hook = @convention(thin) (UnownedJob, Original) - > Void let swift_task_enqueueGlobal_hook = dlsym( dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook" ) .assumingMemoryBound(to: Hook ? . self) swift_task_enqueueGlobal_hook.pointee = { job, original in print("hooked") original(job) } Task { print("oh hi mark") } // Prints: // hooked // oh hi mark
  22. dlsym Access internals of your dependencies typealias Original = @convention(thin)

    (UnownedJob) - > Void typealias Hook = @convention(thin) (UnownedJob, Original) - > Void let swift_task_enqueueGlobal_hook = dlsym( dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook" ) .assumingMemoryBound(to: Hook ? . self) swift_task_enqueueGlobal_hook.pointee = { job, original in print("hooked") original(job) } Task { print("oh hi mark") } // Prints: // hooked // oh hi mark pointfree.co/ep240
  23. Why do this? • Reach into your dependencies internals to

    customize / fi x their functionality
  24. Why not do this? • Library internals are not part

    of its API contract • Can break at any time • No compile-time feedback if broken
  25. How does it work? • Embed dynamic framework in your

    app’s bundle • But don’t link it with your app binary • Just like any other static asset* • *But it’s actually a code-signed executable
  26. How does it work? • Problem: you can’t import ThirdParty

    • Problem: you can’t call into it the framework normally 🤔
  27. How does it work? • You can then call into

    functions using the same dlsym tricks shown earlier • Codegen tools can help automate this • Alternatively you can use the Objective-C runtime • Alternatively, use - weak_link and rename the framework inside the bundle before manually dlopen-ing it (h/t Kabir)
  28. Why do this? • Prevent untrusted 3rd party code from

    running • Some 3rd party libraries do really sketchy stu ff • Don’t let your dependencies control you
  29. Why not do this? • Increases distribution size • Dynamic

    libraries: no linker dead code stripping • Kind of a lot of work and who’s got the time for that?
  30. dlsym Dynamically choose dependencies struct DynamicLinkLibrary { fileprivate let handle:

    UnsafeMutableRawPointer func load<T>(symbol: String) throws - > T { if let sym = dlsym(handle, symbol) { return unsafeBitCast(sym, to: T.self) } throw LoaderError.noSymbol( symbol: symbol, error: String(validatingUTF8: dlerror()) ) } } struct Loader { let searchPaths: [String] func load(path: String) throws - > DynamicLinkLibrary { let fullPaths = searchPaths .map { $0.appending(pathComponent: path) } .filter { $0.isFile } for fullPath in fullPaths + [path] { if let handle = dlopen(fullPath, RTLD_LAZY) { return DynamicLinkLibrary(handle: handle) } } throw LoaderError.failed(path: path) } }
  31. Why do this? • Dynamically choose dependencies • A/B test

    two di ff erent versions of a dependency • For Mac/CLI apps: • Use a dependency they already have • Reduce distribution size • Avoids duplicate symbol con fl icts
  32. Why not do this? • Can increase distribution size •

    Dynamic libraries: no linker dead code stripping • Double the libraries: double the size • Kind of a lot of work and who’s got the time for that?
  33. Refactoring @_spi(…) // Soup Module // Old func makeSoup() {}

    // New func makeSoupBetterAndImproved() {}
  34. validate { "X-12- 34- 5 |" $0.inputs[0].buffer(policy: .bufferingOldest(2)) "X,,,[1,],,[2,],[3,][5,]|" }

    Testing Using DSLs for testing and beyond apple/swift-async-algorithms
  35. assertParse( "{ 1⃣return 0 }", { ExprSyntax.parse(from: &$0) }, substructure:

    ReturnStmtSyntax( returnKeyword: .keyword(.return), expression: IntegerLiteralExprSyntax(literal: .integerLiteral("0")) ), substructureAfterMarker: "1⃣" ) Testing Using DSLs for testing and beyond apple/swift-syntax
  36. Example(""" values.↓reduce(Array<Int>()) { result, value in result += [value] }

    """) Testing Using DSLs for testing and beyond SwiftLint
  37. Pattern Recap • Generics • Trampolines • Warning Traps •

    dlsym • Accessing Internals • Deferred Loading • Dynamic Dependency Resolution • Refactoring • SPI • Testing • DSLs • External Datasets
  38. My iPhone has 2 million times the storage of the

    1969 Apollo 11 computer. They went to the moon. I throw birds at pig houses. – Bill Murray “ ”