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: https://www.droidcon.com/2022/09/29/dynamic-code-with-zipline/

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

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

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

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

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

    • Lets you use C and C++ libraries from the JVM or Android!
  11. @Test fun runJavaScript() { val quickJs = QuickJs.create() assertEquals( "Hello

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

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

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

    .joinToString(separator = " ") } src / jsMain / kotlin / com / example / hello.kt
  16. @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
  17. Idea • Running Kotlin/JS in your app is neat, but

    clumsy • What if it worked like Retrofit?
  18. API class Zipline { fun <T : ZiplineService> bind(name: String,

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

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

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

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

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

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

    WorldClockService : ZiplineService { fun formatTime(location: Location): String src / commonMain / kotlin / com / example / WorldClockService.kt
  26. 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 }
  27. 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) },
  28. Kotlin Serialization • Bridged calls use JSON as an intermediate

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

    String { val jsValue = encodeToDynamic(serializer, value) return JSON.stringify(jsValue) }
  30. 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
  31. 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
  32. 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
  33. ...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
  34. @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
  35. @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
  36. Kotlin/JS Source Code Running in QuickJS .js files .js files

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

    CDN build Kotlin/JS Gradle Plugin Your Upload Task Zipline Loader
  38. 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" ] }
  39. 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
  40. 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
  41. .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
  42. 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
  43. ZiplineLoader • Takes the URL of a manifest • Returns

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

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

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

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

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

    • In Production: • Load cached/embedded code if launched offline • Update to fresh code once connected
  51. Using Zipline + MPP • Shared inte aces and types

    in commonMain • Implementations in hostMain, or androidMain + iosMain
  52. 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
  53. 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
  54. Kotlin Line Numbers #1 bytecode files .kt files .js files

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

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

    .js.map files Kotlin/JS Gradle Plugin Zipline Gradle Plugin
  57. 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
  58. Code Signing #4 zipline { ... signingKeys { create("key1") {

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

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

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

    'JavaScript'].join(' ')" ) as String }
  63. 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)
  64. COOL THINGS 5 Kotlin Line Numbers Sampling Profiler Not Closed

    Detection 1. 2. 3. 4. 5. Code Signing No Eval
  65. 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
  66. No Debugging • Zipline doesn’t (yet) have a step-debugger •

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

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

    • We’re gonna experiment with a Kotlin/WASM backend
  69. Is This You? • Too much speculative code? • Too

    many feature flags? • Slow feedback loops? • Backwards-compatibility hell?