· 7 min read Posted by Gustavo Fão Valvassori
Kotlin/Wasm interop with Javascript
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.
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!")
}
@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
}
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?)
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)")
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.