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

Dynamic Code With Zipline

Jesse Wilson
September 01, 2022

Dynamic Code With Zipline

Video: [coming soon]

As products grow, teams tend to move business logic to the backend. Keeping clients dumb avoids duplication and allows behavior changes without app releases. But it comes with significant downsides: limited interactivity, difficult development, and inflexible APIs.

Zipline is a new library from Cash App that takes a new approach. Instead of moving logic to the backend, Zipline runs dynamic Kotlin/JS code in your Android and iOS apps. It lets you ship behavior changes without an app release!

This talk advises when to use dynamic code and how to adopt it in your apps. It also goes deep on Zipline internals:

- Interface bridging
- Coroutines & Flows
- Fast launches
- Debugging features

If your apps are getting dumber, or you’re using server-driven UI, don’t miss this talk for a great alternative.

Jesse Wilson

September 01, 2022
Tweet

More Decks by Jesse Wilson

Other Decks in Technology

Transcript

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

  2. Dynamic Code

  3. SEPT 2022 DEC 2022 MAR 2023 JUNE 2023 SEPT 2023

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

    SEPT 2023
  5. CHANGE PRICINGe BECOMEe AD-FUNDED SEPT 2022 DEC 2022 MAR 2023

    JUNE 2023 SEPT 2023
  6. 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
  7. We Don’t Know The Future • So our apps prepare

    for many possibilities • Feature flags • Server-driven UI • Loose & Lenient APIs
  8. 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
  9. The Web Solved This in the ’90s Every user is

    always on the latest version It’s awesome for customers, engineers, velocity N
  10. Demo

  11. JS

  12. 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
  13. JAVASCRIPT SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' ');

    } IN: QuickJS
  14. 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
  15. 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
  16. JAVASCRIPT SOURCE function sayHello() { return ['Hello', 'JavaScript'] .join(' ');

    } IN: OUT: "Hello JavaScript" EVALUATION RESULT QuickJS
  17. JNI Bindings for QuickJS • JNI: Java Native Inte ace

    • Lets you use C and C++ libraries from the JVM or Android!
  18. https://speakerdeck.com/swankjesse/jni-hello-world

  19. @Test fun runJavaScript() { val quickJs = QuickJs.create() assertEquals( "Hello

    JavaScript", quickJs.evaluate("['Hello', 'JavaScript'].join(' ')"), ) quickJs.close() }
  20. Kotlin/JS • Run Kotlin in your browser • Or any

    modern JavaScript engine, like QuickJS
  21. 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
  22. Kotlin/JS is JS • Syntax: dynamic, js("...") • Stdlib: Console.log,

    JSON.stringify() • Tools: Webpack, Terser • Libraries: either NPM or Kotlin/Multiplatform
  23. package com.example @JsExport fun sayHello(): String { return listOf("Hello", "Kotlin/JS")

    .joinToString(separator = " ") } src / jsMain / kotlin / com / example / hello.kt
  24. @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
  25. Inte ace Bridging

  26. Idea • Running Kotlin/JS in your app is neat, but

    clumsy • What if it worked like Retrofit?
  27. https://jakewharton.com/using-kotlin-js-and-native-on-android/

  28. API class Zipline { fun <T : ZiplineService> bind(name: String,

    instance: T) fun <T : ZiplineService> take(name: String): T ... companion object { fun create(...): Zipline } }
  29. interface WorldClockService { fun formatTime(location: Location): String }x src /

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

    src / commonMain / kotlin / com / example / WorldClockService.kt
  31. fun prepare(zipline: Zipline) { zipline.bind("clock", RealWorldClockService()) } class RealWorldClockService :

    WorldClockService { override fun formatTime(location: Location): String { ... } } src / jsMain / kotlin / com / example / RealWorldClockService.kt
  32. src / androidMain / kotlin / com / example /

    ClockPresenter.kt class ClockPresenter( zipline: Zipline ) { val service = zipline.take<WorldClockService>("clock") fun displayTime(): String { return service.formatTime(Location(...)) } }
  33. 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
  34. interface WorldClockService : ZiplineService {ab fun formatTime(location: Location): String }x

    src / commonMain / kotlin / com / example / WorldClockService.kt
  35. companion object { internal object Adapter : ZiplineServiceAdapter<WorldClockService>() {yyy interface

    WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt
  36. companion object { internal object Adapter : ZiplineServiceAdapter<WorldClockService>() {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 }
  37. companion object { internal object Adapter : ZiplineServiceAdapter<WorldClockService>() {yyy interface

    WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt override val functions = listOf( object : ZiplineFunction<WorldClockService>( "fun formatTime(location: Location): String", ) { override fun call( service: WorldClockService, args: List<*> ) = service.formatTime(args[0] as Location) },
  38. Kotlin Serialization • Bridged calls use JSON as an intermediate

    representation • And kotlinx.serialization to get that JSON
  39. Fast-enough JSON fun <T> Json.encodeToStringFast( serializer: KSerializer<T>, value: T, ):

    String { val jsValue = encodeToDynamic(serializer, value) return JSON.stringify(jsValue) }
  40. Suspension Bridges interface NetworkTimeService : ZiplineService { } suspend fun

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

    ) Instant callback: SuspendCallback< >
  42. Pass-by-Reference • How do we encode a SuspendCallback as JSON?

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

    • By building another bridge!
  44. 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
  45. Flows interface WorldClockService : ZiplineService { fun formatNow(): Flow<String> }

  46. 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
  47. 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
  48. ...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
  49. Packaging

  50. @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
  51. @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
  52. Kotlin/JS Source Code Running in QuickJS ? ? ?

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

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

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

    CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader
  56. .js files build

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

    build
  58. 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" ] }
  59. 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
  60. Remember how QuickJS Works?

  61. 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
  62. .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
  63. 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
  64. ZiplineLoader • Takes the URL of a manifest • Returns

    a running Zipline (QuickJS + bridges)
  65. Loading Strategy 1. Fetch a manifest 2. Fetch all the

    bytecode, in parallel 3. Load bytecode into QuickJS as it arrives
  66. Caching • We don’t update our kotlin stdlib every day

    • Only download it when it changes! • Same for all modules
  67. 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": {} } }
  68. 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" },
  69. Embedded Code • Embed the latest bytecode + manifest in

    the APK • Minimize downloads on most launches • Something to fall back to when running offline
  70. Flows of Code val ziplineFlow: Flow<LoadedZipline> = ziplineLoader.load( applicationName =

    "world-clock", manifestUrlFlow = repeatFlow(manifestUrl, 500L), )
  71. Always Fresh Code • In Development: • Load on save

    • In Production: • Load cached/embedded code if launched offline • Update to fresh code once connected
  72. Zipline On iOS

  73. Multiplatformization • Zipline is a regular Kotlin/Multiplatform library • QuickJS

    runs natively on iOS, no JNI required!
  74. Android App Multiplatform Libraries Downloadable Code

  75. Downloadable Code iOS App Multiplatform Libraries

  76. httpClient: OkHttpClient, fun ZiplineLoader( dispatcher: CoroutineDispatcher, manifestVerifier: ManifestVerifier, eventListener: EventListener

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

    = EventListener.NONE, nowEpochMs: () -> Long = systemEpochMsClock, ): ZiplineLoader
  78. context: Context, fileSystem: FileSystem, directory: Path, maxSizeInBytes: Long, ): ZiplineCache

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

  80. Using Zipline + MPP • Shared inte aces and types

    in commonMain • Implementations in hostMain, or androidMain + iosMain
  81. COOL THINGS 5

  82. java.lang.Exception: unexpected timezone: America/Gotham at captureStack (runtime/coreRuntime.kt:86) at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)

    at <anonymous> (app.cash.worldclock/TimeFormatter.kt:98) at <anonymous> (app.cash.worldclock/WorldClockJs.kt:615) at <anonymous> (InboundService.kt:837) at <anonymous> (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
  83. java.lang.Exception: unexpected timezone: America/Gotham at captureStack (runtime/coreRuntime.kt:86) at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)

    at <anonymous> (app.cash.worldclock/TimeFormatter.kt:98) at <anonymous> (app.cash.worldclock/WorldClockJs.kt:615) at <anonymous> (InboundService.kt:837) at <anonymous> (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
  84. Kotlin Line Numbers #1 bytecode files .kt files .js files

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

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

    .js.map files Kotlin/JS Gradle Plugin Zipline Gradle Plugin
  87. val hprofFile = File("fibonacci.hprof") zipline.quickJs.startCpuSampling(hprofFile).use { // ...use Zipline... }

    Sampling Profiler #2
  88. Sampling Profiler #2

  89. 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
  90. Code Signing #4 zipline { ... signingKeys { create("key1") {

    privateKeyHex.set("ae4737d95df505eac2424000559d0") algorithmId.set(Ed25519) } create("key2") { privateKeyHex.set("6207b6f19c9d7dfa8af31ed5d9789") algorithmId.set(EcdsaP256) } } }
  91. "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 {
  92. "unsigned": { "signatures": { "key1": "0f91508b8451a8ed4eedf723f22613fe", "key2": "ff105ef4a5bc691614c3b8b472808e2b" } },

    "modules": { "atomicfu.js": { "sha256": "5eb408f3363d431344133ef5" }, "kotlin-stdlib.js": { "sha256": "f3363d431344133ef55eb408" }, "kotlinx-serialization.js": { "sha256": "4133ef55eb408f3363d43134" Code Signing #4 {
  93. Code Signing #4 val verifier = ManifestVerifier.Builder() .addEd25519("key1", "6207b6f19c9d7dfa8af3d5d".decodeHex()) .addEcdsaP256("key2",

    "3041020100301306072a88c".decodeHex()) .build() val ziplineLoader = ZiplineLoader( manifestVerifier = verifier, dispatcher = ..., httpClient = ..., )
  94. No Eval #5 fun getMessage(): String { return eval( "['Hello',

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

    'JavaScript'].join(' ')" ) as String } app.cash.zipline.QuickJsException: eval is not supported at JavaScript.<eval>(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)
  96. COOL THINGS 5 Kotlin Line Numbers Sampling Profiler Not Closed

    Detection 1. 2. 3. 4. 5. Code Signing No Eval
  97. Concerns

  98. 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
  99. No Debugging • Zipline doesn’t (yet) have a step-debugger •

    You have to build for Kotlin/JVM and debug there
  100. Rules are Rules • Zipline lets you change behavior of

    already-released apps • Follow Apple’s and Google’s rules
  101. Powered by JavaScript? • But you never see the JavaScript

    • We’re gonna experiment with a Kotlin/WASM backend
  102. Next Steps

  103. Is This You? • Too much speculative code? • Too

    many feature flags? • Slow feedback loops? • Backwards-compatibility hell?
  104. Zipline Status • We’re close to shipping 1.0 • In

    Cash App this fall!
  105. Thanks @jakewha on @jessewilson https://github.com/cashapp/zipline/