与 JavaScript 的互操作

Kotlin/Wasm allows you to both use JavaScript code from Kotlin and Kotlin code from JavaScript.

The Kotlin/JS compiler already provides the ability to transpile your Kotlin code to JavaScript. The Kotlin/Wasm interoperability with JavaScript is designed in a similar way, taking into account that JavaScript is a dynamically typed language compared to Kotlin. Follow our guide to configure interoperability in your projects.

Remember that Kotlin/Wasm is still Experimental, and some features are not supported yet. We’re planning to improve interoperability with JavaScript by implementing some of the missing features or similar functionality.

Use JavaScript code from Kotlin

external modifier

To access JavaScript declarations defined in the global scope, mark them with the external modifier. Consider this JavaScript code sample:

  1. // JavaScript
  2. function consoleLogExample() {
  3. console.log("Hello");
  4. }
  5. let externalInt = 0;
  6. let Counter = {
  7. value: 0,
  8. step: 1,
  9. increment() {
  10. this.value += this.step;
  11. }
  12. };
  13. class Rectangle {
  14. constructor(height, width) {
  15. this.height = height;
  16. this.width = width;
  17. }
  18. get area() {
  19. return this.calcArea();
  20. }
  21. calcArea() {
  22. return this.height * this.width;
  23. }
  24. }

Here’s how you can use this JavaScript code in Kotlin:

  1. // Kotlin/Wasm
  2. // Use external functions to call JS functions defined in global scope
  3. external fun consoleLogExample(): Unit
  4. // In addition to functions, you can have external top-level properties
  5. external var externalInt: Int
  6. // External objects
  7. external object Counter {
  8. fun increment(): Unit
  9. val value: Int
  10. var step: Int
  11. }
  12. // External class
  13. external class Rectangle(height: Double, width: Double) {
  14. val height: Double
  15. val width: Double
  16. val area: Double
  17. fun calcArea(): Double
  18. }

See the full code in the example project Kotlin/Wasm browser.

Some “external” Kotlin/JS features are not supported in Kotlin/Wasm:

  • Implementing or extending external types
  • External enum classes

与 JavaScript 的互操作 - 图1

@JsFun annotation

To include a small piece of JS code in your Kotlin/Wasm module, use the @JsFun annotation with external top-level functions. The annotation argument should be a string with JS code that evaluates a function with a matching signature:

  1. @JsFun("function count(x) { return x + 10; }")
  2. external fun count(x: Int): Int

To make it shorter, use arrow syntax:

  1. @JsFun("x => x + 10")
  2. external fun count(x: Int): Int

The Kotlin compiler doesn’t verify these JavaScript snippets and evaluates them as-is. Syntax errors, if any, will be reported when running your JavaScript.

These function expressions are evaluated only once, before the Wasm module is loaded. Do not rely on side effects as these expressions are not run if the function is not called.

与 JavaScript 的互操作 - 图2

@JsModule

To indicate that an external class, package, function, or property is a JavaScript module, use the @JsModule annotation. Consider this JavaScript code sample:

  1. // jsModule.mjs
  2. let maxUsers = 10;
  3. function getActiveUsers() {
  4. return 10;
  5. };
  6. class User {
  7. constructor(maxUsers) {
  8. this.maxUsers = maxUsers;
  9. }
  10. }
  11. export {maxUsers, getActiveUsers, User};

Here’s how you can use this JavaScript code in Kotlin:

  1. // kotlin
  2. @file:JsModule("./jsModule.mjs")
  3. package example
  4. external val maxUsers: Int
  5. external fun getActiveUsers(): Int
  6. external class User {
  7. constructor(username: String)
  8. val username : String
  9. }

Kotlin/Wasm supports ES modules only. That’s why you can’t use the @JsNonModule annotation.

与 JavaScript 的互操作 - 图3

Use Kotlin code from JavaScript

@JsExport annotation

To make the Kotlin/Wasm declaration available from JavaScript, use the @JsExport annotation with external top-level functions:

  1. // Kotlin/Wasm
  2. @JsExport
  3. fun addOne(x: Int): Int = x + 1

Now you can use this function from JavaScript in the following way:

  1. // JavaScript
  2. import exports from "module.mjs"
  3. exports.addOne(10)

Functions marked at @JsExport are visible as properties on a default export of the generated .mjs module. Kotlin types in JavaScript In Kotlin/JS, values are implemented internally as JavaScript primitives and objects. They are passed to and from JavaScript without wrapping or copying.

However, in Kotlin/Wasm, objects have a different representation and are not interchangeable with JavaScript. When you pass a Kotlin object to JavaScript, it’s considered as an empty opaque object by default.

The only thing you can do is store it and pass Kotlin objects back to Wasm. However, for primitive types, Kotlin/Wasm can adapt these values so that they can be useful in JavaScript by either copying or wrapping. For efficiency purposes, this is done statically. It’s important that these special concrete types are present in function signatures. For example:

  1. external fun convertIntAndString(num: Int, text: String)
  2. external fun convertAnyAndChars(num: Any, text: CharSequence)
  3. // ...
  4. convertIntAndString(10, "Hello") // Converts Int and String to JS Number and String
  5. convertAnyAndChars(10, "Hello") // No conversion
  6. // values are passed as opaque references to Wasm objects

Kotlin types in JavaScript

Supported types

See how Kotlin types are mapped to JavaScript ones:

KotlinJavaScriptComments
Byte, Short, Int, CharNumber
Float, DoubleNumber
LongBigInt
BooleanBoolean
StringStringString content is copied. In the future, the stringref proposal could allow the zero-copy string interop.
UnitUndefinedOnly when non-nullable and in functions returning position.
Function type, for example (int, String) → IntFunction referenceParameters and return values of function types follow the same type of conversion rules.
external interfaceAny JS value with given properties
external class or external objectCorresponding JS class
Other Kotlin typesNot supportedThis includes type Any, arrays, the Throwable class, collections, and so on.
Nullable Type?Type / null / undefined
Type parameters <T : U>Same as the upper boundIn interop declarations, only external types, like JsAny, are supported as upper bounds of type parameters.

Exception handling

The Kotlin/Wasm try-catch expression can’t catch the JavaScript exceptions.

If you try to use JavaScript try-catch expression to catch the Kotlin/Wasm exceptions, it’ll look like a generic WebAssembly.Exception without directly accessible messages and data.

Workarounds for Kotlin/JS features non-supported in Kotlin/Wasm

Dynamic type

Kotlin/JS dynamic type used for interoperability with untyped or loosely typed objects is not supported yet. In many cases, you can use external interfaces and the @JsFun annotation instead:

  1. // Kotlin/JS
  2. val user: dynamic
  3. val age: Int = 0
  4. user.profile.updateAge(age);
  5. // Kotlin/Wasm
  6. external interface User
  7. @JsFun("(user, age) => user.profile.updateAge(age)")
  8. external fun updateUserAge(user: User, age: Int)
  9. val user: User
  10. val age: Int = 0
  11. updateUserAge(user, age);

Inline JavaScript

The js() function used to inline JavaScript code to Kotlin code is not supported yet. Use the @JsFun annotation instead:

  1. // Kotlin/JS
  2. fun jsTypeOf(obj: Any): String {
  3. return js("typeof obj")
  4. }
  5. // Kotlin/Wasm
  6. @JsFun("(obj) => typeof obj")
  7. external fun jsTypeOf(obj: SomeExternalInterfaceType): String

Extending external interfaces and classes with non-external classes

Extending JavaScript classes and using external interfaces is not supported yet. Use the @JsFun annotation instead:

  1. external interface DataProcessor {
  2. fun processData(input: String): String
  3. fun processResult(input: String): String
  4. }
  5. class DataHandler(val handlerData: String) {
  6. fun processData(input: String): String = input + handlerData
  7. fun processResult(input: String): String = handlerData + input
  8. }
  9. @JsFun("(processData, processResult) => ({ processData, processResult })")
  10. external fun createDataProcessor(
  11. processData: (String) -> String,
  12. processResult: (String) -> String
  13. ): DataProcessor
  14. fun convertHandlerToProcessor(handler: DataHandler): DataProcessor =
  15. createDataProcessor(
  16. processData = { input -> handler.processData(input) },
  17. processResult = { input -> handler.processResult(input) }
  18. )