· 7 min read Posted by Gustavo Fão Valvassori

Kotlin/Wasm interop with Javascript

Kotlin/Wasm provides a bi-directional interoperability between Kotlin and Javascript, allowing you to call functions on either side. Let's dive into the topic and explore its limitations and workarounds.
Claudio Schwarz - https://unsplash.com/photos/two-human-hands-painting-k39RGHmLoV8
Credit: Claudio Schwarz - https://unsplash.com/photos/two-human-hands-painting-k39RGHmLoV8

Just like Kotlin’s Native and JS implementations, Kotlin/Wasm offers bi-directional interoperability. This means that you can both call Javascript methods from Kotlin and vice-versa. In this article, we will explore both sides and a bit of its limitations.

Short disclaimer: the Kotlin/Wasm APIs are still experimental and may change in the future. This post describes the state using the 1.9.22/23 version of Kotlin.

Calling Kotlin/Wasm methods from Javascript

To call a Kotlin/Wasm method from Javascript, you need to make things “public”, but this process has a few gotchas and limitations. The first one is that it follow most of the same rules from Kotlin/JS presented in this post. So if you don’t mark a function as “exported”, they will not be visible. In other words, functions that you want to call from the JS side, you must add the @JsExport annotation to it.

@JsExport
fun sayHello() {
    println("Hello from Kotlin!")
}
Kotlin 1.9.20 also introduced the @WasmExport method, but it is used for WasmWasi, not WasmJS.

When you compile your Kotlin/Wasm module, it generates a JS file that you can import in your HTML. This file is responsible for loading the Wasm module and exposing it to the window. After importing it on your HTML, you can get your module from the window object and call the visible functions.

const { default: WasmModule } = await window['wasm-getting-started'] // Import it
WasmModule.sayHello() // Call the function

Unfortunately, the exportation has some limitations. In the Kotlin/Wasm target you can only export top-level functions, primitive types and strings. In other words, you can’t export classes, objects, or any other complex type created on the Kotlin side. This rule also applies for function parameters and return types.

// Using primitive types work fine
@JsExport
fun sum(a: Int, b: Int): Int {
    return a + b
}

// This does not compile as parameter is unsupported
@JsExport
fun printMessage(message: Message) {
    println(message.value)
}

// Using it as return type is not supported either
@JsExport
fun getMessage(): Message {
    return Message("Hello from Kotlin!")
}

data class Message(val value: String)

Another limitation is about the packages. The wasm exports does not support packages, so if you have two functions with the same name in different packages, you will have a name collision. When the compiler detects a name collision, it will throw an error as each function must have an individual name. As a workaround you can rename one of them using the @JsName annotation.

@JsExport
@JsName("sumInt")
fun sum(a: Int, b: Int): Int {
    return a + b
}

@JsExport
@JsName("sumFloat")
fun sum(a: Float, b: Float): Float {
    return a + b
}

The last thing you should be aware of, is with JS Types. Even though Kotlin/Wasm does not support complex types, you can still use types defined on the Javascript side. We will talk about it in the next section.

External modifier

Similar to Kotlin/JS, Kotlin/Wasm allows you to use the external modifier on classes, functions, and variables. Using it allows us to call Javascript code from the Kotlin/Wasm side. This modifier works for properties and classes defined in your own Javascript code or already existing browser APIs. Here is an example with a property and class defined in the index.html file.

<html>
    <head>
        <script lang="javascript">
            let myJsVar = 42

            class Person {
                constructor(name, age) {
                    this.name = name
                    this.age = age
                }
            }

            function copyPerson(person) {
                return new Person(person.name, person.age)
            }
        </script>
        <script src="my-kotlin-wasm-project.js"></script>
    </head>
</html>

And the Kotlin counterpart of it will look like this:

external var myJsVar: Int

external class Person(name: String, age: Int) : JsAny {
    val name: String
    val age: Int
}

external fun copyPerson(person: Person): Person

fun main() {
    print("Value from my JS var: $myJsVar")
    print(Person("Gustavo", 28))
    print(copyPerson(Person("Gustavo", 28)))
}

Types defined on the JS side can be passed to Kotlin methods and returned from them. All you need to do is make sure it inherits from JsAny and you are good to go.

// Send JS type as argument 
@JsExport
fun printPerson(person: Person) {
    println("Name: ${person.name}, Age: ${person.age}")
}

// Return JS type
@JsExport
fun fetchPerson(id: String): Person {
    val person: Person = // TODO: Fetch from API
    return person
}
The JsAny inheritance is optional. But to use as arguments or pass it to JS, your type must implement this interface.

Annotating with JsFun

As previously mentioned in the Kermit Now Supports WASM post, you can use the @JsFun annotation to create JS functions directly inside in the Kotlin code. This is useful in the console.log example where you don’t want to write the complete Console interface using the external modifier.

@JsFun("(output) => console.log(output)")
external fun consoleLog(vararg output: JsAny?)
From Kotlin 1.9.20 version forward, using the Any type is not allowed anymore. If you need to use the Any type you can replace with the JsAny version.

Running JS Code on Kotlin

Another interop feature is the ability to run Javascript code from Kotlin. This is done using the js function. It allows you to run any Javascript code directly from your Kotlin code. It has the same effect from the @JsFun annotation, but you write the code directly in the method body.

fun showAlert() {
    js("alert('Hello from Kotlin!')")
}

This method also allows you to use the function arguments as parameter inside the Javascript code. So you could use it to show an alert with a dynamic message.

fun showAlert(message: String) {
    js("alert(message)")
}

Lastly, it can also return data created in the JS world.

fun createPerson(name: String, age: Int): Person =
    js("new Person(name, age)")
This function can only be called individually. Using it in the middle of another method will not work!

Built-in interops

To help you avoid repetition, a few of the most common external APIs are already implemented for you. One example is the kotlinx.browser package that provides external types for browser APIs.

// Source: https://github.com/JetBrains/kotlin/blob/1.9.20/libraries/stdlib/wasm/js/src/kotlinx/browser/declarations.kt
package kotlinx.browser

import org.w3c.dom.*

external val window: Window
external val document: Document
external val localStorage: Storage
external val sessionStorage: Storage

With those declarations, you don’t need to manually declare them. These declarations also include child values, like the window.fetch that can be used to make HTTP requests.

How does it work?

In the previous article, we mentioned the generated “glue code” for the wasm interop. When we use any of the techniques presented in this article, the Kotlin compiler generates the necessary glue code to make the interop work.

So if you use the @JsExport annotation, your method will be available in the exports from the wasm instance:

export async function instantiate(imports={}, runInitializer=true) {
    
    // Rest of the code

    let wasmInstance;
    let wasmExports;

    try {
        if (isNodeJs) {
            // Run methods for NodeJS
        }
        
        if (isStandaloneJsVM) {
            // Run methods for standalone JS VM
        }
    
        if (isBrowser) {
            // Load Wasm file 
            wasmInstance = (await WebAssembly.instantiateStreaming(fetch(wasmFilePath), importObject)).instance;
        }
    } catch (e) {
      // Error handling for the Wasm instantiation
    }
    
    // Get the exports from the instance 
    wasmExports = wasmInstance.exports;
    
    // Return the exports with the instance. 
    // This `wasmExports` will be visible with the default key 
    return { instance: wasmInstance,  exports: wasmExports };
}

When you use the external modifier, it will generate the required bridge methods. So the person examples will be converted into the following code:

export async function instantiate(imports={}, runInitializer=true) {
    // Rest of the code

    // Declared bridge methods
    const js_code = {
        // External variable
        'myJsVar_$external_prop_getter' : () => myJsVar,
        'myJsVar_$external_prop_setter' : (v) => myJsVar = v,

        // External function
        'copyPerson_$external_fun' : (p0) => copyPerson(p0),

        // Person Class
        'Person_$external_fun' : (p0, p1) => new Person(p0, p1), // Constructor
        'Person_$external_class_instanceof' : (x) => x instanceof Person,
    }
    
    // Rest of the code
}

Lastly, for the @JsFun annotation and js() method, the JS code will be directly inserted into the generated glue code. So the consoleLog and showAlert examples will be converted into the following code:

export async function instantiate(imports={}, runInitializer=true) {
    // Rest of the code

    // Declared bridge methods
    const js_code = {
        // @JsFun
        'consoleLog' : (output) => console.log(output),

        // js() method
        'showAlert' : () => { alert('Hello from Kotlin!') }, // Without arguments
        'showAlert_1' : (message) => { alert(message) }, // With Arguments
    }
    
    // Rest of the code
}

Final thoughts

In conclusion, Kotlin provides bi-directional interoperability between Kotlin/Wasm and Javascript, allowing you to call functions on either side. However, there are some limitations to keep in mind, such as the inability to export complex types created on Kotlin. You have a few workarounds, like moving some types to JS and use of the external modifier to access them from Kotlin.