Open4

Kotlin/Wasm JS interop

tanishikingtanishiking

https://kotlinlang.org/docs/wasm-js-interop.html

https://github.com/Kotlin/kotlin-wasm-examples/tree/068c94953eb47fef47e800f49d203b4a4286e8bc/browser-example

diff --git a/browser-example/build.gradle.kts b/browser-example/build.gradle.kts
index de77ccc..d7eae38 100644
--- a/browser-example/build.gradle.kts
+++ b/browser-example/build.gradle.kts
@@ -47,3 +47,10 @@ kotlin {
         val wasmJsTest by getting
     }
 }
+
+
+tasks.withType<org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile>().configureEach {
+    kotlinOptions.freeCompilerArgs += listOf(
+        "-Xwasm-generate-wat",
+    )
+}
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.dom.appendElement
import kotlinx.dom.appendText
import org.w3c.dom.Element
import org.w3c.dom.HTMLInputElement

fun main() {
    document.body?.appendElement("div") {
        appendText("Time in ")

        val output = document.createElement("span")

        val input = appendElement("input") {
            this as HTMLInputElement
            type = "text"
            placeholder="Timezone"
            
            value = "Europe/Amsterdam"

            addEventListener("change") {
                updateTime(this, output)
            }
        } as HTMLInputElement

        appendText(" is ")
        appendChild(output)

        updateTime(input, output)
    }
}

val progress = "⡆⠇⠋⠙⠸⢰⣠⣄".map(Char::toString)

private fun updateTime(input: HTMLInputElement, output: Element) {
    var i = 0
    val progressId = window.setInterval({
        output.textContent = progress[i]
        i = (i + 1) % progress.size
        null
    }, 100)
    
    window.fetch("https://worldtimeapi.org/api/timezone/${input.value}")
        .then {
            window.clearInterval(progressId)

            if (it.ok) {
                it.json().then {
                    output.textContent = (it as WorldTimeApiResponse).datetime
                        ?.substringAfter("T")?.substringBefore(".") ?: "🧐"
                    null
                }
            } else {
                output.textContent = "🤷 " + it.status
            }
            null
        }
        .catch {
            window.clearInterval(progressId)
            output.textContent = "🙅🛜"
            null
        }
}

external interface WorldTimeApiResponse {
    val datetime: String?
}
$ ./gradlew build

build/js/packages/kotlin-wasm-browser-example-wasm-js/kotlin/kotlin-wasm-browser-example-wasm-js.uninstantiated.mjs

export async function instantiate(imports={}, runInitializer=true) {
    const externrefBoxes = new WeakMap();
    // ref must be non-null
    function tryGetOrSetExternrefBox(ref, ifNotCached) {
        if (typeof ref !== 'object') return ifNotCached;
        const cachedBox = externrefBoxes.get(ref);
        if (cachedBox !== void 0) return cachedBox;
        externrefBoxes.set(ref, ifNotCached);
        return ifNotCached;
    }


    
    const js_code = {
        'kotlin.captureStackTrace' : () => new Error().stack,
        'kotlin.wasm.internal.throwJsError' : (message, wasmTypeName, stack) => { 
            const error = new Error();
            error.message = message;
            error.name = wasmTypeName;
            error.stack = stack;
            throw error;
             },
        'kotlin.wasm.internal.stringLength' : (x) => x.length,
        'kotlin.wasm.internal.jsExportStringToWasm' : (src, srcOffset, srcLength, dstAddr) => { 
            const mem16 = new Uint16Array(wasmExports.memory.buffer, dstAddr, srcLength);
            let arrayIndex = 0;
            let srcIndex = srcOffset;
            while (arrayIndex < srcLength) {
                mem16.set([src.charCodeAt(srcIndex)], arrayIndex);
                srcIndex++;
                arrayIndex++;
            }     
             },
        'kotlin.wasm.internal.importStringFromWasm' : (address, length, prefix) => { 
            const mem16 = new Uint16Array(wasmExports.memory.buffer, address, length);
            const str = String.fromCharCode.apply(null, mem16);
            return (prefix == null) ? str : prefix + str;
             },
        'kotlin.wasm.internal.getJsEmptyString' : () => '',
        'kotlin.wasm.internal.externrefToString' : (ref) => String(ref),
        'kotlin.wasm.internal.isNullish' : (ref) => ref == null,
        'kotlin.wasm.internal.newJsArray' : () => [],
        'kotlin.wasm.internal.jsArrayPush' : (array, element) => { array.push(element); },
        'kotlin.wasm.internal.tryGetOrSetExternrefBox_$external_fun' : (p0, p1) => tryGetOrSetExternrefBox(p0, p1),
        'kotlin.js.then_$external_fun' : (_this, p0) => _this.then(p0),
        'kotlin.js.__convertKotlinClosureToJsClosure_((Js?)->Js?)' : (f) => (p0) => wasmExports['__callFunction_((Js?)->Js?)'](f, p0),
        'kotlin.js.__convertKotlinClosureToJsClosure_((Js)->Js?)' : (f) => (p0) => wasmExports['__callFunction_((Js)->Js?)'](f, p0),
        'kotlin.js.catch_$external_fun' : (_this, p0) => _this.catch(p0),
        'kotlin.random.initialSeed' : () => ((Math.random() * Math.pow(2, 32)) | 0),
        'kotlinx.browser.document_$external_prop_getter' : () => document,
        'kotlinx.browser.window_$external_prop_getter' : () => window,
        'org.w3c.dom.encryptedmedia.__convertKotlinClosureToJsClosure_((Js)->Unit)' : (f) => (p0) => wasmExports['__callFunction_((Js)->Unit)'](f, p0),
        'org.w3c.dom.events.Event_$external_class_instanceof' : (x) => x instanceof Event,
        'org.w3c.dom.events.addEventListener_$external_fun' : (_this, p0, p1) => _this.addEventListener(p0, p1),
        'org.w3c.dom.Element_$external_class_instanceof' : (x) => x instanceof Element,
        'org.w3c.dom.body_$external_prop_getter' : (_this) => _this.body,
        'org.w3c.dom.createElement_$external_fun' : (_this, p0, p1, isDefault0) => _this.createElement(p0, isDefault0 ? undefined : p1, ),
        'org.w3c.dom.createTextNode_$external_fun' : (_this, p0) => _this.createTextNode(p0),
        'org.w3c.dom.placeholder_$external_prop_setter' : (_this, v) => _this.placeholder = v,
        'org.w3c.dom.type_$external_prop_setter' : (_this, v) => _this.type = v,
        'org.w3c.dom.value_$external_prop_getter' : (_this) => _this.value,
        'org.w3c.dom.value_$external_prop_setter' : (_this, v) => _this.value = v,
        'org.w3c.dom.HTMLInputElement_$external_class_instanceof' : (x) => x instanceof HTMLInputElement,
        'org.w3c.dom.ownerDocument_$external_prop_getter' : (_this) => _this.ownerDocument,
        'org.w3c.dom.textContent_$external_prop_setter' : (_this, v) => _this.textContent = v,
        'org.w3c.dom.appendChild_$external_fun' : (_this, p0) => _this.appendChild(p0),
        'org.w3c.dom.__convertKotlinClosureToJsClosure_(()->Js?)' : (f) => () => wasmExports['__callFunction_(()->Js?)'](f, ),
        'org.w3c.dom.setInterval_$external_fun' : (_this, p0, p1, p2, isDefault0, isDefault1) => _this.setInterval(p0, isDefault0 ? undefined : p1, ...p2, ),
        'org.w3c.dom.clearInterval_$external_fun' : (_this, p0, isDefault0) => _this.clearInterval(isDefault0 ? undefined : p0, ),
        'org.w3c.dom.fetch_$external_fun' : (_this, p0, p1, isDefault0) => _this.fetch(p0, isDefault0 ? undefined : p1, ),
        'org.w3c.fetch.status_$external_prop_getter' : (_this) => _this.status,
        'org.w3c.fetch.ok_$external_prop_getter' : (_this) => _this.ok,
        'org.w3c.fetch.json_$external_fun' : (_this, ) => _this.json(),
        'org.w3c.fetch.Response_$external_class_instanceof' : (x) => x instanceof Response,
        'datetime_$external_prop_getter' : (_this) => _this.datetime
    }
    
    // Placed here to give access to it from externals (js_code)
    let wasmInstance;
    let require; 
    let wasmExports;

    const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node');
    const isStandaloneJsVM =
        !isNodeJs && (
            typeof d8 !== 'undefined' // V8
            || typeof inIon !== 'undefined' // SpiderMonkey
            || typeof jscOptions !== 'undefined' // JavaScriptCore
        );
    const isBrowser = !isNodeJs && !isStandaloneJsVM && (typeof window !== 'undefined');
    
    if (!isNodeJs && !isStandaloneJsVM && !isBrowser) {
      throw "Supported JS engine not detected";
    }
    
    const wasmFilePath = './kotlin-wasm-browser-example-wasm-js.wasm';
    const importObject = {
        js_code,

    };
    
    try {
      if (isNodeJs) {
        const module = await import(/* webpackIgnore: true */'node:module');
        require = module.default.createRequire(import.meta.url);
        const fs = require('fs');
        const path = require('path');
        const url = require('url');
        const filepath = url.fileURLToPath(import.meta.url);
        const dirpath = path.dirname(filepath);
        const wasmBuffer = fs.readFileSync(path.resolve(dirpath, wasmFilePath));
        const wasmModule = new WebAssembly.Module(wasmBuffer);
        wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
      }
      
      if (isStandaloneJsVM) {
        const wasmBuffer = read(wasmFilePath, 'binary');
        const wasmModule = new WebAssembly.Module(wasmBuffer);
        wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
      }
      
      if (isBrowser) {
        wasmInstance = (await WebAssembly.instantiateStreaming(fetch(wasmFilePath), importObject)).instance;
      }
    } catch (e) {
      if (e instanceof WebAssembly.CompileError) {
        let text = `Please make sure that your runtime environment supports the latest version of Wasm GC and Exception-Handling proposals.
For more information, see https://kotl.in/wasm-help
`;
        if (isBrowser) {
          console.error(text);
        } else {
          const t = "\n" + text;
          if (typeof console !== "undefined" && console.log !== void 0) 
            console.log(t);
          else 
            print(t);
        }
      }
      throw e;
    }
    
    wasmExports = wasmInstance.exports;
    if (runInitializer) {
        wasmExports._initialize();
    }

    return { instance: wasmInstance,  exports: wasmExports };
}

build/js/packages/kotlin-wasm-browser-example-wasm-js/kotlin/kotlin-wasm-browser-example-wasm-js.wat

tanishikingtanishiking

とりあえず windowdocument

        'kotlinx.browser.document_$external_prop_getter' : () => document,
        'kotlinx.browser.window_$external_prop_getter' : () => window,

    (func $kotlinx.browser.document_$external_prop_getter___fun_16 (import "js_code" "kotlinx.browser.document_$external_prop_getter") (type $____type_12))
    (func $kotlinx.browser.window_$external_prop_getter___fun_17 (import "js_code" "kotlinx.browser.window_$external_prop_getter") (type $____type_12))

    (type $____type_12 (func (param) (result externref)))

tanishikingtanishiking

JSExport

https://kotlinlang.org/docs/wasm-js-interop.html#functions-with-the-jsexport-annotation

@JsExport
fun addOne(x: Int): Int = x + 1
(export "addOne" (func $addOne___fun_60))

    (func $addOne___fun_60 (type $____type_1)
        (param $0_x i32) (result i32)
        (local $1_currentIsNotFirstWasmExportCall i32)
        (local $2_tmp i32)
        (local $3_e (ref null $kotlin.Throwable___type_33))
        (local $4_t (ref null $kotlin.Throwable___type_33))
        global.get $kotlin.wasm.internal.isNotFirstWasmExportCall___g_4  ;; type: kotlin.Boolean
        local.set $1_currentIsNotFirstWasmExportCall
        
        ;; Inlined call of `<UNKNOWN>`
        block (result (ref null $kotlin.Unit___type_26))
            
            ;; Inlined call of `<UNKNOWN>`
            block (result i32)
                try
                    try
                        i32.const 1
                        global.set $kotlin.wasm.internal.isNotFirstWasmExportCall___g_4  ;; type: kotlin.Boolean
                        local.get $0_x  ;; type: kotlin.Int
                        i32.const 1
                        i32.add
                        br 2
                    catch 0
                        local.set $3_e
                        local.get $1_currentIsNotFirstWasmExportCall  ;; type: kotlin.Boolean
                        if (result (ref null $kotlin.Unit___type_26))
                            local.get $3_e  ;; type: kotlin.Throwable
                            throw 0
                        else
                            local.get $3_e  ;; type: kotlin.Throwable
                            call $kotlin.wasm.internal.throwAsJsException___fun_51
                            unreachable
                        end
                        br 3
                    end
                    unreachable
                catch 0
                    local.set $4_t
                    local.get $1_currentIsNotFirstWasmExportCall  ;; type: kotlin.Boolean
                    global.set $kotlin.wasm.internal.isNotFirstWasmExportCall___g_4  ;; type: kotlin.Boolean
                    local.get $4_t  ;; type: kotlin.Throwable
                    throw 0
                end
                unreachable
            end
            
            local.set $2_tmp
            local.get $1_currentIsNotFirstWasmExportCall  ;; type: kotlin.Boolean
            global.set $kotlin.wasm.internal.isNotFirstWasmExportCall___g_4  ;; type: kotlin.Boolean
            local.get $2_tmp  ;; type: kotlin.Int
            return
        end
        
        drop
        local.get $1_currentIsNotFirstWasmExportCall  ;; type: kotlin.Boolean
        global.set $kotlin.wasm.internal.isNotFirstWasmExportCall___g_4  ;; type: kotlin.Boolean
        unreachable)
tanishikingtanishiking

Any とか使える?

@JsExport
fun addOne(x: Any): Any = x

e: file:///Users/tanishiking/ghq/github.com/Kotlin/kotlin-wasm-examples/nodejs-example/src/wasmJsMain/kotlin/Main.kt:4:1 Type Any cannot be used in external function return. Only external, primitive, string and function types are supported in Kotlin/Wasm JS interop.
e: file:///Users/tanishiking/ghq/github.com/Kotlin/kotlin-wasm-examples/nodejs-example/src/wasmJsMain/kotlin/Main.kt:5:12 Type Any cannot be used in external function parameter. Only external, primitive, string and function types are supported in Kotlin/Wasm JS interop.

Type Any cannot be used in external function return. Only external, primitive, string and function types are supported in Kotlin/Wasm JS interop

いいね