Slide 1

Slide 1 text

@jakewha on @jessewilson https://github.com/cashapp/zipline/ Dynamic Code with Zipline

Slide 2

Slide 2 text

Dynamic Code

Slide 3

Slide 3 text

SEPT 2022 DEC 2022 MAR 2023 JUNE 2023 SEPT 2023

Slide 4

Slide 4 text

CHANGE PRICINGe SEPT 2022 DEC 2022 MAR 2023 JUNE 2023 SEPT 2023

Slide 5

Slide 5 text

CHANGE PRICINGe BECOMEe AD-FUNDED SEPT 2022 DEC 2022 MAR 2023 JUNE 2023 SEPT 2023

Slide 6

Slide 6 text

CHANGE PRICINGe LAUNCHe ‘STORIES’ PIVOT TO VIDEOe BECOMEe AD-FUNDED ACQUIRED BY GOOGLE NEW PRIVACY LAWe SEPT 2022 DEC 2022 MAR 2023 JUNE 2023 SEPT 2023

Slide 7

Slide 7 text

We Don’t Know The Future • So our apps prepare for many possibilities • Feature flags • Server-driven UI • Loose & Lenient APIs

Slide 8

Slide 8 text

Backends are Stuck in the Past • Testing old client versions? So much work • Changing an API? So much work • Testing server-driven UI? So much work SEPT 2021 DEC 2021 MAR 2022 JUNE 2022 SEPT 2022

Slide 9

Slide 9 text

The Web Solved This in the ’90s Every user is always on the latest version It’s awesome for customers, engineers, velocity N

Slide 10

Slide 10 text

Demo

Slide 11

Slide 11 text

JS

Slide 12

Slide 12 text

QuickJS • An embeddable JavaScript engine • 65,560 lines of C • 1.2 MiB native library in your APK • Authored by Fabrice Bellard and Charlie Gordon

Slide 13

Slide 13 text

JAVASCRIPT SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' '); } IN: QuickJS

Slide 14

Slide 14 text

JAVASCRIPT SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' '); } hello.js:1: function: sayHello mode: strict stack_size: 3 opcodes: push_atom_value "Hello" push_atom_value "JavaScript" array_from 2 get_field2 join push_atom_value " " tail_call_method 1 IN: OUT: QUICKJS BYTECODE QuickJS Compiler

Slide 15

Slide 15 text

hello.js:1: function: sayHello mode: strict stack_size: 3 opcodes: push_atom_value "Hello" push_atom_value "JavaScript" array_from 2 get_field2 join push_atom_value " " tail_call_method 1 IN: OUT: QUICKJS BYTECODE "Hello JavaScript" EVALUATION RESULT QuickJS Interpreter

Slide 16

Slide 16 text

JAVASCRIPT SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' '); } IN: OUT: "Hello JavaScript" EVALUATION RESULT QuickJS

Slide 17

Slide 17 text

JNI Bindings for QuickJS • JNI: Java Native Inte ace • Lets you use C and C++ libraries from the JVM or Android!

Slide 18

Slide 18 text

https://speakerdeck.com/swankjesse/jni-hello-world

Slide 19

Slide 19 text

@Test fun runJavaScript() { val quickJs = QuickJs.create() assertEquals( "Hello JavaScript", quickJs.evaluate("['Hello', 'JavaScript'].join(' ')"), ) quickJs.close() }

Slide 20

Slide 20 text

Kotlin/JS • Run Kotlin in your browser • Or any modern JavaScript engine, like QuickJS

Slide 21

Slide 21 text

Kotlin/JS is Kotlin • Syntax: nullability, data classes, named & default parameters, extension functions, annotations, lambdas • Stdlib: strings, collections, transforms, sequences, ranges, regexes, firstNotNullOfOrNull • Tools: IntelliJ, Gradle • Libraries: Compose, Coroutines, Serialization, Okio

Slide 22

Slide 22 text

Kotlin/JS is JS • Syntax: dynamic, js("...") • Stdlib: Console.log, JSON.stringify() • Tools: Webpack, Terser • Libraries: either NPM or Kotlin/Multiplatform

Slide 23

Slide 23 text

package com.example @JsExport fun sayHello(): String { return listOf("Hello", "Kotlin/JS") .joinToString(separator = " ") } src / jsMain / kotlin / com / example / hello.kt

Slide 24

Slide 24 text

@Test fun sayHello() { val quickJs = QuickJs.create() quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js") quickJs.loadJsModule("./hello-kotlin-js.js") assertEquals( "Hello Kotlin/JS", quickJs.evaluate( "require('./hello-kotlin-js.js').com.example.sayHello()" ) ) quickJs.close() } src / jvmTest / kotlin / com / example / HelloTest.kt

Slide 25

Slide 25 text

Inte ace Bridging

Slide 26

Slide 26 text

Idea • Running Kotlin/JS in your app is neat, but clumsy • What if it worked like Retrofit?

Slide 27

Slide 27 text

https://jakewharton.com/using-kotlin-js-and-native-on-android/

Slide 28

Slide 28 text

API class Zipline { fun bind(name: String, instance: T) fun take(name: String): T ... companion object { fun create(...): Zipline } }

Slide 29

Slide 29 text

interface WorldClockService { fun formatTime(location: Location): String }x src / commonMain / kotlin / com / example / WorldClockService.kt

Slide 30

Slide 30 text

interface WorldClockService : ZiplineService { fun formatTime(location: Location): String }x src / commonMain / kotlin / com / example / WorldClockService.kt

Slide 31

Slide 31 text

fun prepare(zipline: Zipline) { zipline.bind("clock", RealWorldClockService()) } class RealWorldClockService : WorldClockService { override fun formatTime(location: Location): String { ... } } src / jsMain / kotlin / com / example / RealWorldClockService.kt

Slide 32

Slide 32 text

src / androidMain / kotlin / com / example / ClockPresenter.kt class ClockPresenter( zipline: Zipline ) { val service = zipline.take("clock") fun displayTime(): String { return service.formatTime(Location(...)) } }

Slide 33

Slide 33 text

Implementation • Zipline has a Kotlin compiler plugin • It generates an adapter to implement bridging • It rewrites bind() and take() calls to pass that adapter • Outbound: implements the inte ace & encodes calls as JSON • Inbound: decodes JSON calls and calls the real implementation

Slide 34

Slide 34 text

interface WorldClockService : ZiplineService {ab fun formatTime(location: Location): String }x src / commonMain / kotlin / com / example / WorldClockService.kt

Slide 35

Slide 35 text

companion object { internal object Adapter : ZiplineServiceAdapter() {yyy interface WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt

Slide 36

Slide 36 text

companion object { internal object Adapter : ZiplineServiceAdapter() {yyy interface WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt override fun outboundService(handler: Handler) = object : WorldClockService { override fun formatTime(location: Location) = handler.call(this, functionIndex = 0, location) as String }

Slide 37

Slide 37 text

companion object { internal object Adapter : ZiplineServiceAdapter() {yyy interface WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt override val functions = listOf( object : ZiplineFunction( "fun formatTime(location: Location): String", ) { override fun call( service: WorldClockService, args: List<*> ) = service.formatTime(args[0] as Location) },

Slide 38

Slide 38 text

Kotlin Serialization • Bridged calls use JSON as an intermediate representation • And kotlinx.serialization to get that JSON

Slide 39

Slide 39 text

Fast-enough JSON fun Json.encodeToStringFast( serializer: KSerializer, value: T, ): String { val jsValue = encodeToDynamic(serializer, value) return JSON.stringify(jsValue) }

Slide 40

Slide 40 text

Suspension Bridges interface NetworkTimeService : ZiplineService { } suspend fun now( : ) Instant

Slide 41

Slide 41 text

Suspension Bridges interface NetworkTimeService : ZiplineService { } fun now( ) Instant callback: SuspendCallback< >

Slide 42

Slide 42 text

Pass-by-Reference • How do we encode a SuspendCallback as JSON? • x

Slide 43

Slide 43 text

Pass-by-Reference • How do we encode a SuspendCallback as JSON? • By building another bridge!

Slide 44

Slide 44 text

Example • To encode: 1. Generate an ID 2. Call bind(id, service) 3. Encode the ID • To decode: 1. Decode the ID 2. Call take(id) 3. Return the service

Slide 45

Slide 45 text

Flows interface WorldClockService : ZiplineService { fun formatNow(): Flow }

Slide 46

Slide 46 text

Flows Pass-By-Reference • Returning a flow? Pass-by-reference of Flow • Collecting a flow? Pass-by-reference back of FlowCollector • Emitting to a collector? Uses the suspending emit() function, which is implemented as pass-by-reference of SuspendCallback

Slide 47

Slide 47 text

Bridging Is Powe ul... • Call Kotlin/JS mostly like a regular function call • Offer data & services to Kotlin JS using the same mechanism • Capable concurrency • You never see the JavaScript • You never see the JSON

Slide 48

Slide 48 text

...But There’s Gotchas! • You need to close() everything you take() • Exceptions are wrapped in a ZiplineException • Code versions might be different! • Be mindful of parameter & response data size

Slide 49

Slide 49 text

Packaging

Slide 50

Slide 50 text

@Test fun sayHello() { val quickJs = QuickJs.create() quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js") quickJs.loadJsModule("./hello-kotlin-js.js") assertEquals( "Hello Kotlin/JS", quickJs.evaluate( "require('./hello-kotlin-js.js').com.example.sayHello()" ) ) quickJs.close() } src / jvmTest / kotlin / com / example / HelloTest.kt

Slide 51

Slide 51 text

@Test fun sayHello() { val quickJs = QuickJs.create() quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js") quickJs.loadJsModule("./hello-kotlin-js.js") assertEquals( "Hello Kotlin/JS", quickJs.evaluate( "require('./hello-kotlin-js.js').com.example.sayHello()" ) ) quickJs.close() } src / jvmTest / kotlin / com / example / HelloTest.kt YUCK

Slide 52

Slide 52 text

Kotlin/JS Source Code Running in QuickJS ? ? ?

Slide 53

Slide 53 text

Kotlin/JS Source Code Running in QuickJS .js files build Kotlin/JS Gradle Plugin

Slide 54

Slide 54 text

Kotlin/JS Source Code Running in QuickJS .js files .js files CDN build Kotlin/JS Gradle Plugin Your Upload Task

Slide 55

Slide 55 text

Kotlin/JS Source Code Running in QuickJS .js files .js files CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader

Slide 56

Slide 56 text

.js files build

Slide 57

Slide 57 text

compileSync main productionExecutable kotlin atomicfu.js kotlin-stdlib.js kotlinx-serialization.js kotlinx-coroutines-core.js hello-kotlin-js.js zipline.js build

Slide 58

Slide 58 text

build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json { "modules": [ "atomicfu.js", "kotlin-stdlib.js", "kotlinx-serialization.js", "kotlinx-coroutines-core.js", "hello-kotlin-js.js", "zipline.js" ] }

Slide 59

Slide 59 text

Kotlin/JS Source Code Running in QuickJS .js files .js files + manifest CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader .js files + manifest build Zipline Gradle Plugin

Slide 60

Slide 60 text

Remember how QuickJS Works?

Slide 61

Slide 61 text

SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' '); } hello.js:1: function: sayHello mode: strict stack_size: 3 opcodes: push_atom_value "Hello" push_atom_value "JavaScript" array_from 2 get_field2 join push_atom_value " " tail_call_method 1 BYTECODE "Hello JavaScript" RESULT QuickJS Compiler QuickJS Interpreter

Slide 62

Slide 62 text

.js source + manifest .js source + manifest Kotlin/JS Source Code Running in QuickJS .js files CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader build Zipline Gradle Plugin

Slide 63

Slide 63 text

bytecode + manifest bytecode + manifest Kotlin/JS Source Code Running in QuickJS .js files CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader build Zipline Gradle Plugin

Slide 64

Slide 64 text

ZiplineLoader • Takes the URL of a manifest • Returns a running Zipline (QuickJS + bridges)

Slide 65

Slide 65 text

Loading Strategy 1. Fetch a manifest 2. Fetch all the bytecode, in parallel 3. Load bytecode into QuickJS as it arrives

Slide 66

Slide 66 text

Caching • We don’t update our kotlin stdlib every day • Only download it when it changes! • Same for all modules

Slide 67

Slide 67 text

build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json { "modules": { "atomicfu.js": {}, "kotlin-stdlib.js": {}, "kotlinx-serialization.js": {}, "kotlinx-coroutines-core.js": {}, "hello-kotlin-js.js": {}, "zipline.js": {} } }

Slide 68

Slide 68 text

build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json { "modules": { "atomicfu.js": { "sha256": "5eb408f3363d431344133ef5" }, "kotlin-stdlib.js": { "sha256": "f3363d431344133ef55eb408" }, "kotlinx-serialization.js": { "sha256": "4133ef55eb408f3363d43134" }, "kotlinx-coroutines-core.js": { "sha256": "d431344133ef55eb408f3363" },

Slide 69

Slide 69 text

Embedded Code • Embed the latest bytecode + manifest in the APK • Minimize downloads on most launches • Something to fall back to when running offline

Slide 70

Slide 70 text

Flows of Code val ziplineFlow: Flow = ziplineLoader.load( applicationName = "world-clock", manifestUrlFlow = repeatFlow(manifestUrl, 500L), )

Slide 71

Slide 71 text

Always Fresh Code • In Development: • Load on save • In Production: • Load cached/embedded code if launched offline • Update to fresh code once connected

Slide 72

Slide 72 text

Zipline On iOS

Slide 73

Slide 73 text

Multiplatformization • Zipline is a regular Kotlin/Multiplatform library • QuickJS runs natively on iOS, no JNI required!

Slide 74

Slide 74 text

Android App Multiplatform Libraries Downloadable Code

Slide 75

Slide 75 text

Downloadable Code iOS App Multiplatform Libraries

Slide 76

Slide 76 text

httpClient: OkHttpClient, fun ZiplineLoader( dispatcher: CoroutineDispatcher, manifestVerifier: ManifestVerifier, eventListener: EventListener = EventListener.NONE, nowEpochMs: () -> Long = systemEpochMsClock, ): ZiplineLoader

Slide 77

Slide 77 text

urlSession: NSURLSession, fun ZiplineLoader( dispatcher: CoroutineDispatcher, manifestVerifier: ManifestVerifier, eventListener: EventListener = EventListener.NONE, nowEpochMs: () -> Long = systemEpochMsClock, ): ZiplineLoader

Slide 78

Slide 78 text

context: Context, fileSystem: FileSystem, directory: Path, maxSizeInBytes: Long, ): ZiplineCache fun ZiplineCache(

Slide 79

Slide 79 text

fileSystem: FileSystem, directory: Path, maxSizeInBytes: Long, ): ZiplineCache fun ZiplineCache(

Slide 80

Slide 80 text

Using Zipline + MPP • Shared inte aces and types in commonMain • Implementations in hostMain, or androidMain + iosMain

Slide 81

Slide 81 text

COOL THINGS 5

Slide 82

Slide 82 text

java.lang.Exception: unexpected timezone: America/Gotham at captureStack (runtime/coreRuntime.kt:86) at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js) at (app.cash.worldclock/TimeFormatter.kt:98) at (app.cash.worldclock/WorldClockJs.kt:615) at (InboundService.kt:837) at (zipline-root-zipline.js) at app.cash.zipline.OutboundService.runJob(platform.kt:37) at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57) at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167) at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:920) Kotlin Line Numbers #1

Slide 83

Slide 83 text

java.lang.Exception: unexpected timezone: America/Gotham at captureStack (runtime/coreRuntime.kt:86) at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js) at (app.cash.worldclock/TimeFormatter.kt:98) at (app.cash.worldclock/WorldClockJs.kt:615) at (InboundService.kt:837) at (zipline-root-zipline.js) at app.cash.zipline.OutboundService.runJob(platform.kt:37) at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57) at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167) at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:920) Kotlin Line Numbers #1

Slide 84

Slide 84 text

Kotlin Line Numbers #1 bytecode files .kt files .js files Kotlin/JS Gradle Plugin Zipline Gradle Plugin

Slide 85

Slide 85 text

Kotlin Line Numbers #1 bytecode files .kt files .js files .js.map files Kotlin/JS Gradle Plugin Zipline Gradle Plugin

Slide 86

Slide 86 text

Kotlin Line Numbers #1 bytecode files .kt files .js files .js.map files Kotlin/JS Gradle Plugin Zipline Gradle Plugin

Slide 87

Slide 87 text

val hprofFile = File("fibonacci.hprof") zipline.quickJs.startCpuSampling(hprofFile).use { // ...use Zipline... } Sampling Profiler #2

Slide 88

Slide 88 text

Sampling Profiler #2

Slide 89

Slide 89 text

java.lang.Exception: helloService created here, was not closed at EventListener.takeService(LoggingEventListener.kt:36) at Endpoint.take(Endpoint.kt:127) at Zipline.take(Zipline.kt:132) at ServiceLeakingTest.test(ServiceLeakingTest.kt:67) Not closed Detection #3

Slide 90

Slide 90 text

Code Signing #4 zipline { ... signingKeys { create("key1") { privateKeyHex.set("ae4737d95df505eac2424000559d0") algorithmId.set(Ed25519) } create("key2") { privateKeyHex.set("6207b6f19c9d7dfa8af31ed5d9789") algorithmId.set(EcdsaP256) } } }

Slide 91

Slide 91 text

"modules": { "atomicfu.js": { "sha256": "5eb408f3363d431344133ef5" }, "kotlin-stdlib.js": { "sha256": "f3363d431344133ef55eb408" }, "kotlinx-serialization.js": { "sha256": "4133ef55eb408f3363d43134" }, "kotlinx-coroutines-core.js": { "sha256": "d431344133ef55eb408f3363" }, "hello-kotlin-js.js": { "sha256": "5eb408f3363d431344133ef5" Code Signing #4 {

Slide 92

Slide 92 text

"unsigned": { "signatures": { "key1": "0f91508b8451a8ed4eedf723f22613fe", "key2": "ff105ef4a5bc691614c3b8b472808e2b" } }, "modules": { "atomicfu.js": { "sha256": "5eb408f3363d431344133ef5" }, "kotlin-stdlib.js": { "sha256": "f3363d431344133ef55eb408" }, "kotlinx-serialization.js": { "sha256": "4133ef55eb408f3363d43134" Code Signing #4 {

Slide 93

Slide 93 text

Code Signing #4 val verifier = ManifestVerifier.Builder() .addEd25519("key1", "6207b6f19c9d7dfa8af3d5d".decodeHex()) .addEcdsaP256("key2", "3041020100301306072a88c".decodeHex()) .build() val ziplineLoader = ZiplineLoader( manifestVerifier = verifier, dispatcher = ..., httpClient = ..., )

Slide 94

Slide 94 text

No Eval #5 fun getMessage(): String { return eval( "['Hello', 'JavaScript'].join(' ')" ) as String }

Slide 95

Slide 95 text

No Eval #5 fun getMessage(): String { return eval( "['Hello', 'JavaScript'].join(' ')" ) as String } app.cash.zipline.QuickJsException: eval is not supported at JavaScript.(test.kt) at app.cash.zipline.QuickJs.execute(Native Method) at app.cash.zipline.QuickJs.execute(QuickJs.kt:147) at app.cash.zipline.QuickJs.evaluate(QuickJs.kt:111) at app.cash.zipline.Test.getMessage(Test.kt:123)

Slide 96

Slide 96 text

COOL THINGS 5 Kotlin Line Numbers Sampling Profiler Not Closed Detection 1. 2. 3. 4. 5. Code Signing No Eval

Slide 97

Slide 97 text

Concerns

Slide 98

Slide 98 text

Bigger Alternatives Exist JS ecosystem Works best when you go all-in JS ecosystem Strada is a rad idea Flutter ecosystem Go all-in React Native Web Views Flutter

Slide 99

Slide 99 text

No Debugging • Zipline doesn’t (yet) have a step-debugger • You have to build for Kotlin/JVM and debug there

Slide 100

Slide 100 text

Rules are Rules • Zipline lets you change behavior of already-released apps • Follow Apple’s and Google’s rules

Slide 101

Slide 101 text

Powered by JavaScript? • But you never see the JavaScript • We’re gonna experiment with a Kotlin/WASM backend

Slide 102

Slide 102 text

Next Steps

Slide 103

Slide 103 text

Is This You? • Too much speculative code? • Too many feature flags? • Slow feedback loops? • Backwards-compatibility hell?

Slide 104

Slide 104 text

Zipline Status • We’re close to shipping 1.0 • In Cash App this fall!

Slide 105

Slide 105 text

Thanks @jakewha on @jessewilson https://github.com/cashapp/zipline/