Slide 1

Slide 1 text

JP Simard • @[email protected] • SwiftTO 23 Unconventional Swift Patterns Tricks to give your project superpowers

Slide 2

Slide 2 text

// HACK: Temporary Workaround *Literally never removed *

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

dlsym generics refactoring testing

Slide 6

Slide 6 text

Generics with Timothée Chalamet

Slide 7

Slide 7 text

Generics Trampolines!

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Why do this? • Don’t have to change your call sites • Keep using your dependencies as you always have

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Generics Silence deprecation warnings

Slide 18

Slide 18 text

Protocols Silence deprecation warnings

Slide 19

Slide 19 text

Protocols Silence deprecation warnings #pragma clang diagnostic push #pragma clang diagnostic ignored 
 “-Wdeprecated-declarations" /* ... code using the deprecated API ... */ #pragma clang diagnostic pop

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Why do this? • Keep your codebase warning-free* • Sometimes deprecated APIs don’t have equivalent replacements

Slide 25

Slide 25 text

Why not do this? • Sweeps warnings under the rug • Deprecated API might be fully removed in the near future and catch you by surprise

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

dlsym with Wes Anderson I make this face when I find code using dlsym

Slide 28

Slide 28 text

dlsym Access internals of your dependencies

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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;

Slide 31

Slide 31 text

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)

Slide 32

Slide 32 text

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) }

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Why do this? • Reach into your dependencies internals to customize / fi x their functionality

Slide 36

Slide 36 text

Why not do this? • Library internals are not part of its API contract • Can break at any time • No compile-time feedback if broken

Slide 37

Slide 37 text

dlsym Defer loading untrusted libraries

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

How does it work? • Problem: you can’t import ThirdParty • Problem: you can’t call into it the framework normally 🤔

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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?

Slide 44

Slide 44 text

dlsym Dynamically choose dependencies

Slide 45

Slide 45 text

dlsym Dynamically choose dependencies struct DynamicLinkLibrary { fileprivate let handle: UnsafeMutableRawPointer func load(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) } }

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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?

Slide 49

Slide 49 text

Refactoring with Tilda Swinton

Slide 50

Slide 50 text

Refactoring @_spi(…)

Slide 51

Slide 51 text

Refactoring @_spi(…) // Soup Module func makeSoup() {}

Slide 52

Slide 52 text

Refactoring @_spi(…) // Soup Module // Old func makeSoup() {} // New func makeSoupBetterAndImproved() {}

Slide 53

Slide 53 text

Refactoring @_spi(…) // Soup Module @_spi(SoupMigration) func makeSoup() {} // New func makeSoupBetterAndImproved() {}

Slide 54

Slide 54 text

Refactoring @_spi(…) // Soup Consumer @_spi(SoupMigration) import Soup makeSoup()

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Testing with Bill Murray

Slide 57

Slide 57 text

Testing Using DSLs for testing and beyond

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Example(""" values.↓reduce(Array()) { result, value in result += [value] } """) Testing Using DSLs for testing and beyond SwiftLint

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

Testing External dataset comparisons

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

Pattern Recap • Generics • Trampolines • Warning Traps • dlsym • Accessing Internals • Deferred Loading • Dynamic Dependency Resolution • Refactoring • SPI • Testing • DSLs • External Datasets

Slide 70

Slide 70 text

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 “ ”

Slide 71

Slide 71 text

Whatever you do, always give 100%. Unless you're donating blood. – Bill Murray “ ”

Slide 72

Slide 72 text

We’re hiring!

Slide 73

Slide 73 text

JP Simard @[email protected] Thank You! 🙏