Skip to content

JavaScriptとの相互運用

Kotlin/Wasmでは、KotlinでJavaScriptコードを使用することも、JavaScriptでKotlinコードを使用することもできます。

Kotlin/JSと同様に、Kotlin/WasmコンパイラもJavaScriptとの相互運用性を持っています。Kotlin/JSの相互運用性に慣れている方なら、Kotlin/Wasmの相互運用性が似ていることに気づくでしょう。ただし、考慮すべき重要な違いがいくつかあります。

NOTE

Kotlin/Wasmはアルファ版です。いつでも変更される可能性があります。本番環境前のシナリオで使用してください。皆様からのフィードバックをYouTrackでいただけると幸いです。

KotlinでJavaScriptコードを使用する

external宣言、JavaScriptコードスニペットを含む関数、および@JsModuleアノテーションを使用して、KotlinでJavaScriptコードを使用する方法を学びます。

External宣言

外部JavaScriptコードは、デフォルトではKotlinから見えません。KotlinでJavaScriptコードを使用するには、external宣言でそのAPIを記述できます。

JavaScript関数

このJavaScript関数を考えてみましょう。

javascript
function greet (name) {
    console.log("Hello, " + name + "!");
}

Kotlinではexternal関数として宣言できます。

kotlin
external fun greet(name: String)

External関数は本体を持たず、通常のKotlin関数として呼び出すことができます。

kotlin
fun main() {
    greet("Alice")
}

JavaScriptプロパティ

このグローバルJavaScript変数を考えてみましょう。

javascript
let globalCounter = 0;

Kotlinでは、外部のvarまたはvalプロパティを使用して宣言できます。

kotlin
external var globalCounter: Int

これらのプロパティは外部で初期化されます。Kotlinコードでは、これらのプロパティに= value初期化子を持たせることはできません。

JavaScriptクラス

このJavaScriptクラスを考えてみましょう。

javascript
class Rectangle {
    constructor (height, width) {
        this.height = height;
        this.width = width;
    }

    area () {
        return this.height * this.width;
    }
}

Kotlinでは外部クラスとして使用できます。

kotlin
external class Rectangle(height: Double, width: Double) : JsAny {
    val height: Double
    val width: Double
    fun area(): Double
}

externalクラス内のすべての宣言は、暗黙的に外部と見なされます。

Externalインターフェース

KotlinでJavaScriptオブジェクトの形状を記述できます。このJavaScript関数とその戻り値を考えてみましょう。

javascript
function createUser (name, age) {
    return { name: name, age: age };
}

その形状がKotlinでexternal interface User型としてどのように記述できるか見てみましょう。

kotlin
external interface User : JsAny {
    val name: String
    val age: Int
}

external fun createUser(name: String, age: Int): User

Externalインターフェースは実行時の型情報を持たず、コンパイル時のみの概念です。したがって、通常のインターフェースと比較して、いくつかの制限があります。

  • isチェックの右辺で使用することはできません。
  • クラスリテラル式 (User::classなど) で使用することはできません。
  • 実体化された型引数として渡すことはできません。
  • asによるexternalインターフェースへのキャストは常に成功します。

Externalオブジェクト

オブジェクトを保持するこれらのJavaScript変数を考えてみましょう。

javascript
let Counter = {
    value: 0,
    step: 1,
    increment () {
        this.value += this.step;
    }
};

Kotlinでは外部オブジェクトとして使用できます。

kotlin
external object Counter : JsAny {
    fun increment()
    val value: Int
    var step: Int
}

External型階層

通常のクラスやインターフェースと同様に、他の外部クラスを拡張し、外部インターフェースを実装するexternal宣言を宣言できます。ただし、同じ型階層内でexternal宣言と非external宣言を混在させることはできません。

JavaScriptコードを含むKotlin関数

= js("code")を本体に持つ関数を定義することで、JavaScriptスニペットをKotlin/Wasmコードに追加できます。

kotlin
fun getCurrentURL(): String =
    js("window.location.href")

JavaScriptステートメントのブロックを実行したい場合は、文字列内のコードを波括弧{}で囲みます。

kotlin
fun setLocalSettings(value: String): Unit = js(
    """{
        localStorage.setItem('settings', value);
}"""
)

オブジェクトを返したい場合は、波括弧{}を括弧()で囲みます。

kotlin
fun createJsUser(name: String, age: Int): JsAny =
    js("({ name: name, age: age })")

Kotlin/Wasmはjs()関数の呼び出しを特別な方法で処理し、その実装にはいくつかの制限があります。

  • js()関数の呼び出しには、文字列リテラル引数が必要です。
  • js()関数の呼び出しは、関数本体内の唯一の式でなければなりません。
  • js()関数は、パッケージレベルの関数からのみ呼び出すことができます。
  • 関数の戻り値の型は明示的に指定する必要があります。
  • は、external funと同様に制限されます。

Kotlinコンパイラは、コード文字列を生成されたJavaScriptファイル内の関数に配置し、WebAssembly形式にインポートします。KotlinコンパイラはこれらのJavaScriptスニペットを検証しません。JavaScriptの構文エラーがある場合、それらはJavaScriptコードを実行したときに報告されます。

NOTE

@JsFunアノテーションも同様の機能を持っており、おそらく非推奨になります。

JavaScriptモジュール

デフォルトでは、external宣言はJavaScriptのグローバルスコープに対応します。Kotlinファイルに@JsModuleアノテーションを付加すると、その中のすべてのexternal宣言は指定されたモジュールからインポートされます。

このJavaScriptコードサンプルを考えてみましょう。

javascript
// users.mjs
export let maxUsers = 10;

export class User {
    constructor (username) {
        this.username = username;
    }
}

このJavaScriptコードをKotlinで@JsModuleアノテーションと共に使用します。

kotlin
// Kotlin
@file:JsModule("./users.mjs")

external val maxUsers: Int

external class User : JsAny {
    constructor(username: String)

    val username: String
}

配列の相互運用

JavaScriptのJsArray<T>をKotlinのネイティブなArrayまたはList型にコピーできます。同様に、これらのKotlin型をJsArray<T>にコピーすることもできます。

JsArray<T>Array<T>に、またはその逆に変換するには、利用可能なアダプター関数のいずれかを使用します。

ジェネリック型間の変換の例を次に示します。

kotlin
val list: List<JsString> =
    listOf("Kotlin", "Wasm").map { it.toJsString() }

// Uses .toJsArray() to convert List or Array to JsArray
val jsArray: JsArray<JsString> = list.toJsArray()

// Uses .toArray() and .toList() to convert it back to Kotlin types 
val kotlinArray: Array<JsString> = jsArray.toArray()
val kotlinList: List<JsString> = jsArray.toList()

型付き配列を対応するKotlin型(例えば、IntArrayInt32Array)に変換するための同様のアダプター関数が利用可能です。詳細な情報と実装については、kotlinx-browserリポジトリを参照してください。

型付き配列間の変換の例を次に示します。

kotlin
import org.khronos.webgl.*

    // ...

    val intArray: IntArray = intArrayOf(1, 2, 3)
    
    // Uses .toInt32Array() to convert Kotlin IntArray to JavaScript Int32Array
    val jsInt32Array: Int32Array = intArray.toInt32Array()
    
    // Uses toIntArray() to convert JavaScript Int32Array back to Kotlin IntArray
    val kotlnIntArray: IntArray = jsInt32Array.toIntArray()

JavaScriptでKotlinコードを使用する

@JsExportアノテーションを使用して、JavaScriptでKotlinコードを使用する方法を学びます。

@JsExportアノテーションを持つ関数

Kotlin/Wasm関数をJavaScriptコードで利用可能にするには、@JsExportアノテーションを使用します。

kotlin
// Kotlin/Wasm

@JsExport
fun addOne(x: Int): Int = x + 1

@JsExportアノテーションが付けられたKotlin/Wasm関数は、生成された.mjsモジュールのdefaultエクスポートのプロパティとして可視化されます。その後、JavaScriptでこの関数を使用できます。

javascript
// JavaScript

import exports from "./module.mjs"

exports.addOne(10)

Kotlin/Wasmコンパイラは、Kotlinコード内のすべての@JsExport宣言からTypeScript定義を生成できます。これらの定義は、IDEやJavaScriptツールでコード補完、型チェックの支援、およびJavaScriptやTypeScriptからKotlinコードを利用しやすくするために使用できます。

Kotlin/Wasmコンパイラは、@JsExportアノテーションが付けられたすべてのトップレベル関数を収集し、.d.tsファイルにTypeScript定義を自動的に生成します。

TypeScript定義を生成するには、build.gradle.ktsファイルのwasmJs{}ブロックにgenerateTypeScriptDefinitions()関数を追加します。

kotlin
kotlin {
    wasmJs {
        binaries.executable()
        browser {
        }
        generateTypeScriptDefinitions()
    }
}

DANGER

Kotlin/WasmでのTypeScript宣言ファイルの生成は実験的です。いつでも削除または変更される可能性があります。

型の対応

Kotlin/Wasmでは、JavaScript相互運用宣言のシグネチャで使用できる型が制限されています。これらの制限は、external= js("code")、または@JsExportを持つ宣言に一律に適用されます。

Kotlinの型がJavaScriptの型にどのように対応するかを見てみましょう。

KotlinJavaScript
Byte, Short, Int, Char, UByte, UShort, UInt,Number
Float, Double,Number
Long, ULong,BigInt
Boolean,Boolean
String,String
戻り値のUnitundefined
関数型(例: (String) -> IntFunction
JsAnyとサブタイプ任意のJavaScript値
JsReferenceKotlinオブジェクトへの不透明な参照
その他の型サポートされていません

これらの型のnullableバージョンも使用できます。

JsAny型

JavaScriptの値は、KotlinではJsAny型とそのサブタイプを使用して表現されます。

Kotlin/Wasm標準ライブラリは、これらの型の一部を表すものを提供しています。

  • パッケージ kotlin.js:
    • JsAny
    • JsBoolean, JsNumber, JsString
    • JsArray
    • Promise

externalインターフェースまたはクラスを宣言することで、カスタムのJsAnyサブタイプを作成することもできます。

JsReference型

Kotlinの値は、JsReference型を使用して不透明な参照としてJavaScriptに渡すことができます。

例えば、このKotlinクラスUserをJavaScriptに公開したい場合:

kotlin
class User(var name: String)

toJsReference()関数を使用してJsReference<User>を作成し、JavaScriptに返すことができます。

kotlin
@JsExport
fun createUser(name: String): JsReference<User> {
    return User(name).toJsReference()
}

これらの参照はJavaScriptでは直接利用できず、空の凍結されたJavaScriptオブジェクトのように振る舞います。これらのオブジェクトを操作するには、get()メソッドを使用して参照値をアンラップする関数をJavaScriptにさらにエクスポートする必要があります。

kotlin
@JsExport
fun setUserName(user: JsReference<User>, name: String) {
    user.get().name = name
}

JavaScriptからクラスを作成し、その名前を変更できます。

javascript
import UserLib from "./userlib.mjs"

let user = UserLib.createUser("Bob");
UserLib.setUserName(user, "Alice");

型パラメーター

JavaScript相互運用宣言は、JsAnyまたはそのサブタイプの上限を持つ場合に型パラメーターを持つことができます。例:

kotlin
external fun <T : JsAny> processData(data: JsArray<T>): T

例外処理

Kotlinのtry-catch式を使用してJavaScriptの例外をキャッチできます。ただし、Kotlin/Wasmでは、スローされた値に関する特定の詳細にデフォルトでアクセスすることはできません。

JsException型に、元のエラーメッセージとJavaScriptからのスタックトレースを含めるように設定できます。そのためには、build.gradle.ktsファイルに以下のコンパイラオプションを追加します。

kotlin
kotlin {
    wasmJs {
        compilerOptions {
            freeCompilerArgs.add("-Xwasm-attach-js-exception")
        }
    }
}

この動作はWebAssembly.JSTag APIに依存しており、これは特定のブラウザでのみ利用可能です。

  • Chrome: バージョン115以降でサポート
  • Firefox: バージョン129以降でサポート
  • Safari: まだサポートされていません

この動作を示す例を次に示します。

kotlin
external object JSON {
    fun <T: JsAny> parse(json: String): T
}

fun main() {
    try {
        JSON.parse("an invalid JSON")
    } catch (e: JsException) {
        println("Thrown value is: ${e.thrownValue}")
        // SyntaxError: Unexpected token 'a', "an invalid JSON" is not valid JSON

        println("Message: ${e.message}")
        // Message: Unexpected token 'a', "an invalid JSON" is not valid JSON

        println("Stacktrace:")
        // Stacktrace:

        // Prints the full JavaScript stack trace 
        e.printStackTrace()
    }
}

-Xwasm-attach-js-exceptionコンパイラオプションを有効にすると、JsException型はJavaScriptエラーからの特定の詳細を提供します。このコンパイラオプションを有効にしない場合、JsExceptionは、JavaScriptコードの実行中に例外がスローされたことを示す一般的なメッセージのみを含みます。

JavaScriptのtry-catch式を使用してKotlin/Wasmの例外をキャッチしようとすると、直接アクセス可能なメッセージやデータのない一般的なWebAssembly.Exceptionのように見えます。

Kotlin/WasmとKotlin/JSの相互運用の違い

Kotlin/Wasmの相互運用性はKotlin/JSの相互運用性と類似していますが、考慮すべき重要な違いがあります。

Kotlin/WasmKotlin/JS
External enums外部enumクラスをサポートしていません。外部enumクラスをサポートしています。
Type extensions外部型を拡張する非外部型をサポートしていません。非外部型をサポートしています。
JsName annotationexternal宣言にアノテーションを付加した場合にのみ効果があります。通常の非外部宣言の名前を変更するために使用できます。
js() functionjs("code")関数呼び出しは、パッケージレベル関数の単一の式本体として許可されます。js("code")関数は、任意のコンテキストで呼び出すことができ、dynamic値を返します。
Module systemsESモジュールのみをサポートします。@JsNonModuleアノテーションに相当するものはありません。そのエクスポートはdefaultオブジェクトのプロパティとして提供されます。パッケージレベルの関数のみのエクスポートを許可します。ESモジュールとレガシーモジュールシステムをサポートします。名前付きESMエクスポートを提供します。クラスとオブジェクトのエクスポートを許可します。
Typesexternal= js("code")、および@JsExportのすべての相互運用宣言に一律に厳格な型制限を適用します。限られた数の組み込みKotlin型とJsAnyサブタイプを許可します。external宣言内のすべての型を許可します。@JsExportで使用できる型を制限します。
Long型はJavaScriptのBigIntに対応します。JavaScriptではカスタムクラスとして可視です。
Arrays相互運用ではまだ直接サポートされていません。代わりに新しいJsArray型を使用できます。JavaScript配列として実装されています。
Other typesKotlinオブジェクトをJavaScriptに渡すにはJsReference<>が必要です。external宣言で非外部Kotlinクラス型を使用できます。
Exception handlingJsExceptionThrowable型で任意のJavaScript例外をキャッチできます。Throwable型を使用してJavaScriptのErrorをキャッチできます。dynamic型を使用して任意のJavaScript例外をキャッチできます。
Dynamic typesdynamic型をサポートしていません。代わりにJsAnyを使用します(以下のサンプルコードを参照)。dynamic型をサポートしています。

NOTE

型なしまたは緩く型付けされたオブジェクトとの相互運用のためのKotlin/JSのdynamic型は、Kotlin/Wasmではサポートされていません。dynamic型の代わりに、JsAny型を使用できます。

kotlin

// Kotlin/JS

fun processUser(user: dynamic, age: Int) {

    // ...

    user.profile.updateAge(age)

    // ...

}

// Kotlin/Wasm

private fun updateUserAge(user: JsAny, age: Int): Unit =

    js("{ user.profile.updateAge(age); }")

fun processUser(user: JsAny, age: Int) {

    // ...

    updateUserAge(user, age)

    // ...

}

Web関連ブラウザAPI

kotlinx-browserライブラリは、JavaScriptブラウザAPIを提供するスタンドアロンライブラリで、以下を含みます。

  • パッケージ org.khronos.webgl:
    • Int8Arrayなどの型付き配列。
    • WebGL型。
  • パッケージ org.w3c.dom.*:
    • DOM API型。
  • パッケージ kotlinx.browser:
    • windowdocumentなどのDOM APIグローバルオブジェクト。

kotlinx-browserライブラリの宣言を使用するには、プロジェクトのビルド設定ファイルに依存関係として追加します。

kotlin
val wasmJsMain by getting {
    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
    }
}