Kermit Now Supports WASM
Tags: kotlinmultiplatform, kotlin, webassembly, logging
Published: 2023-10-30 19:09:32

## What is Kotlin/Wasm?

For Kotlin Multiplatform developers, the conversation has shifted to WebAssembly — a notable addition to the Kotlin Multiplatform (KMP) ecosystem. [WebAssembly]((https://en.wikipedia.org/wiki/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](https://caniuse.com/wasm).

> 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](https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format).

While WebAssembly isn't a newcomer, ongoing developments, such as the anticipated [WebAssembly Garbage Collection (WASM GC)](https://github.com/WebAssembly/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**.

```kotlin
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]((https://kotlinlang.org/docs/wasm-js-interop.html)) 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
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](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/-console/) in Wasm. In Kermit, we had the [ConsoleActual](https://github.com/touchlab/Kermit/blob/1fca73033063b3a52faa14b886d9e5422d2fca49/kermit-core/src/jsMain/kotlin/co/touchlab/kermit/ConsoleWriter.kt#L52) 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:

```kotlin
expect object ConsoleActual : ConsoleIntf
```

2. 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](https://kotlinlang.org/docs/wasm-js-interop.html#inline-javascript) 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**

```kotlin
@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:

```kotlin
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](https://github.com/Kotlin/kotlin-wasm-examples/tree/main/browser-example), we created the [wasm-browser](https://github.com/touchlab/Kermit/tree/main/samples/sample/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](https://kotlinlang.slack.com/archives/CTJB58X7X) channel on Kotlin Slack. Also, you can reach out to me at [@shaktiman_droid](https://twitter.com/shaktiman_droid) on Twitter(X) or [LinkedIn](https://www.linkedin.com/in/shaktiman-droid/). Happy coding!