· 5 min read Posted by Jigar Brahmbhatt

Kermit Now Supports WASM

KMP is getting real WASM support, and Touchlab has been diving in. This post will discuss Touchlab’s experience with WASM, WASM in Kermit, and show an example of using WASM in an existing codebase.
Image from https://www.rawpixel.com/image/3336862
Credit: Image from https://www.rawpixel.com/image/3336862

What is Kotlin/Wasm?

For Kotlin Multiplatform developers, the conversation has shifted to WebAssembly — a notable addition to the Kotlin Multiplatform (KMP) ecosystem. WebAssembly, often abbreviated as WASM, is a binary instruction format tailored for web browsers. Its main goal is to execute high-performance code at speeds close to native applications. WebAssembly enjoys broad support across modern browsers, boasting approximately 96% coverage according to caniuse.

While it’s not meant to be directly written, there’s a fascinating aspect called WebAssembly Text Format (.wat). This format allows developers to represent code in a readable text format. For those curious minds, delve deeper into this format by reading Mozilla’s guide.

While WebAssembly isn’t a newcomer, ongoing developments, such as the anticipated WebAssembly Garbage Collection (WASM GC), make it an evolving technology. Kotlin, recognizing the potential of WebAssembly, has embraced it with the Kotlin/WASM compiler. The Kotlin team is actively ensuring they stay ahead of the curve with the Kotlin/WASM compiler, already incorporating support for features like WASM GC. What makes Kotlin/WASM noteworthy is its ability to allow developers to write code in their preferred language and seamlessly compile it into a WebAssembly binary.

Kermit Steps into the Wasm Arena

Given Kermit’s popularity in the Kotlin Multiplatform (KMP) realm, the community asked for Wasm support. It was exciting to gain firsthand experience with WASM for integration into Kermit.

Step #1 - Adding the Wasm Target

The initial step seemed straightforward: add the wasm target in our build scripts. However, reality hit when we discovered that some of Kermit’s dependencies lacked support for Wasm. This underscored a critical lesson for library developers: ensure upstream libraries are compatible with all KMP targets your project aims to include.

import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

kotlin {
    ...
    @OptIn(ExperimentalWasmDsl::class)
    wasm {
        browser()
        binaries.executable()
    }
}

You can also add nodejs and d8 environment support inside the wasm target, but exploration around that is out of scope for this post.

Step #2 - Common Source Set

Understanding the possibility for interoperability between Kotlin/JS and Kotlin/Wasm, we defined a common source set shared between js and wasm. This approach ensures code reusability between the two targets.

kotlin {
    sourceSets {
        val jsAndWasmMain by creating {
            dependsOn(commonMain)
            jsMain.dependsOn(this)
            wasmMain.dependsOn(this)
        }
        val jsAndWasmTest by creating {
            dependsOn(commonTest)
            jsTest.dependsOn(this)
            wasmTest.dependsOn(this)
        }
    }
}

Step #3 - Code Migration

Migrating code from jsMain to jsAndWasmMain wasn’t a simple copy-paste endeavor. Here, we encountered two challenges:

Problem #1: Console API Unavailability in Wasm

The first hurdle we encountered was the absence of the console API in Wasm. In Kermit, we had the ConsoleActual object in JS, utilizing the console API to log data to the browser’s console. To address this, we took the following steps:

  1. Converted the ConsoleActual object to an expect object, establishing a mandatory object for JS and Wasm:
expect object ConsoleActual : ConsoleIntf
  1. Moved the existing object with console calls to jsMain and created a new one for wasmMain.

Problem #2: Wasm-JS Interop Limitations

To call console from Wasm, we needed to invoke JavaScript from Wasm. While the usual js() for inline JavaScript works for JS, Wasm required a different approach. The @JSFun annotation proved useful, allowing us to execute JavaScript functions from Wasm. However, we encountered a problem with use of Any? in the interface methods:

Wasm.ConsoleActual.kt

@JsFun("(output) => console.error(output)")
external fun consoleError(vararg output: Any?)

internal actual object ConsoleActual : ConsoleIntf {
    override fun error(vararg output: Any?) {
        consoleError(*output)
    }
}

Unfortunately, an inline error surfaced:

Type Any? cannot be used in an external function parameter. Only external, primitive, string, and function types are supported in Kotlin/Wasm JS interop.

To circumvent this limitation, we opted for a design change, using String instead of Any? for both JS and Wasm, as they share the same interface:

internal interface ConsoleIntf {
    fun error(o: String)
    // other methods...
}

This adjustment allowed smooth interaction between Wasm and JS, ensuring compatibility with the restricted set of supported APIs in Kotlin/Wasm.

Step #4 - Integration

Drawing inspiration from Kotlin’s wasm-browser-sample, we created the wasm-browser sample app. It mirrors the module structure of the app-browser app in our Kotlin/JS sample.

Lessons Learned

Our foray into Kotlin/Wasm integration was not without challenges. Key takeaways include:

  • Interoperability with JS is a work in progress, requiring careful consideration when sharing code between the two.
  • Loading generated .wasm files directly in web apps may pose challenges, especially around types. We couldn’t get this to work in the short time we had.
  • Debugging Wasm-related issues can be tricky; Our tests failure output wasn’t easy to understand. Kotlin Slack proved to be an invaluable resource for troubleshooting. We learned that output with --info flag shows the actual node command that’s being executed. Calling that command directly showed better errors and we could fix the code then.
  • Compatibility concerns arise regarding the version of Node.js; Note that versions 1.9.20 onwards would require Node version and Browsers that support Wasm GC.

Feel free to explore the latest version of Kermit and dive into the exciting world of Kotlin/Wasm. Stay tuned for future updates and enhancements! Share your experiences and questions with us on #touchlab-tools channel on Kotlin Slack. Happy coding!