6.2.3.6 支持的版本与配置

原文链接: https://kotlinlang.org/docs/wasm-configuration.html

6.2.3.6 支持的版本与配置

Beta

本页提供 WebAssembly 提案、受支持的浏览器,以及用 Kotlin/Wasm 高效开发的配置建议等详细信息。

浏览器版本

Kotlin/Wasm 依赖最新的 WebAssembly 提案,例如垃圾回收 (WasmGC)和异常处理,以在 WebAssembly 中引入改进和新特性。

为确保这些特性正常工作,请提供支持最新提案的环境。请检查你的浏览器版本是否默认支持新的 WasmGC,或者是否需要对环境做调整。

Chrome

  • 119 或更高版本:

默认可用。

  • 更早的版本:

注意: 要在更旧的浏览器中运行应用,你需要低于 1.9.20 的 Kotlin 版本。

  1. 在浏览器中访问 chrome://flags/#enable-webassembly-garbage-collection。
  2. 启用 WebAssembly Garbage Collection。
  3. 重新启动浏览器。

基于 Chromium 的浏览器

包括基于 Chromium 的浏览器,例如 Edge、Brave、Opera 或 Samsung Internet。

  • 119 或更高版本:

默认可用。

  • 更早的版本:

注意: 要在更旧的浏览器中运行应用,你需要低于 1.9.20 的 Kotlin 版本。

使用 --js-flags=--experimental-wasm-gc 命令行参数运行应用。

Firefox

  • 120 或更高版本:

默认可用。

  • 119 版本:
  1. 在浏览器中访问 about:config。
  2. 启用 javascript.options.wasm_gc 选项。
  3. 刷新页面。

Safari/WebKit

  • 18.2 或更高版本:

默认可用。

  • 更早的版本:

不支持。

注意: Safari 18.2 适用于 iOS 18.2、iPadOS 18.2、visionOS 2.2、macOS 15.2、macOS Sonoma 和 macOS Ventura。在 iOS 和 iPadOS 上,Safari 18.2 随操作系统一起提供。要获得它,请把设备更新到 18.2 或更高版本。更多信息请参阅 Safari 发行说明。

Wasm 提案支持

Kotlin/Wasm 的改进基于 WebAssembly 提案。这里你可以找到关于 WebAssembly 垃圾回收和(旧式)异常处理提案支持情况的详细信息。

垃圾回收提案

从 Kotlin 1.9.20 开始,Kotlin 工具链使用最新版本的 Wasm 垃圾回收(WasmGC)提案。

因此,我们强烈建议把 Wasm 项目升级到最新版本的 Kotlin。我们也建议你使用带 Wasm 环境的最新版本浏览器。

异常处理提案

Kotlin 工具链同时支持异常处理提案的旧式和新版版本。这让 Kotlin 生成的 Wasm 二进制文件能在更广泛的环境中运行。

wasmJs 目标默认使用旧式异常处理提案。要为 wasmJs 目标启用新的异常处理提案,请使用 -Xwasm-use-new-exception-proposal 编译器选项。

相比之下,wasmWasi 目标默认使用新提案,从而确保与现代 WebAssembly 运行时更好地兼容。要切换到旧式提案,请使用 -Xwasm-use-new-exception-proposal=false 编译器选项。

对于 wasmWasi 目标来说,采用新的异常处理提案是安全的。面向该环境的应用通常运行在差异较小的运行时环境中(往往运行在某个特定虚拟机上),而且通常由用户自己控制,从而降低了兼容性问题的风险。

提示: 通过我们的 Kotlin/Wasm 示例进一步了解如何搭建项目、使用依赖以及完成其他任务。

使用默认导入

把 Kotlin/Wasm 代码导入 JavaScript已经转向具名导出,不再使用默认导出。

如果你仍然想使用默认导入,请生成一个新的 JavaScript 包装模块。创建一个内容如下的 .mjs 文件:

1
2
3
4
// 指定主 .mjs 文件的路径
import * as moduleExports from "./wasm-test.mjs";

export { moduleExports as default };

你可以把新的 .mjs 文件放在 resources 文件夹中,构建过程中它会自动被放在主 .mjs 文件旁边。

你也可以把 .mjs 文件放在自定义位置。此时,你需要手动把它移到主 .mjs 文件旁边,或者调整 import 语句中的路径以匹配它的位置。

Kotlin/Wasm 增量编译

Kotlin/Wasm 目标支持增量编译,它让编译器只重新编译受近期更改影响的文件。这有助于减少编译时间。

Wasm 目标的增量编译默认启用。要禁用它,请在项目的 local.properties 或 gradle.properties 文件中添加以下行:

1
kotlin.incremental.wasm=false

完全限定类名中的诊断

在 Kotlin/Wasm 上,为避免增大应用体积,编译器默认不会把类的完全限定名(FQN)存入生成的二进制文件。

因此,在 Kotlin/Wasm 项目中调用 KClass::qualifiedName 属性时,编译器会报错,除非你显式启用完全限定名特性。

这一诊断默认启用,错误会自动报告。要禁用该诊断并允许在 Kotlin/Wasm 中使用 qualifiedName,请指示编译器为所有类存储完全限定名,把这行选项添加到你的 build.gradle.kts 文件中:

1
2
3
4
5
6
7
8
9
// build.gradle.kts
kotlin {
   wasmJs {
       ...
       compilerOptions {
           freeCompilerArgs.add("-Xwasm-kclass-fqn")
       }
   }
}

请记住,启用该选项会增加应用体积。

完全限定名

在 Kotlin/Wasm 目标上,完全限定名(FQN)在运行时无需任何额外配置即可使用。这意味着 KClass.qualifiedName 属性默认启用。

使用 FQN 可以提升代码从 JVM 到 Wasm 目标的可移植性,并通过显示完整的限定名让运行时错误信息更有用。

数组越界访问与陷阱

在 Kotlin/Wasm 中,用越界索引访问数组会触发 WebAssembly 陷阱(trap),而不是普通的 Kotlin 异常。该陷阱会立即停止当前执行栈。

在 JavaScript 环境中运行时,这些陷阱会表现为 WebAssembly.RuntimeError,可以在 JavaScript 一侧捕获。

你可以在 Kotlin/Wasm 环境中通过在链接可执行文件时使用以下编译器选项来避免这类陷阱:

-Xwasm-enable-array-range-checks

或者把它添加到 Gradle 构建文件的 compilerOptions {} 块中:

1
2
3
4
5
6
// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xwasm-enable-array-range-checks")
    }
}

启用该编译器选项后,会抛出 IndexOutOfBoundsException 而不是触发陷阱。

更多细节和反馈请见这个 YouTrack 问题。

实验性注解

Kotlin/Wasm 提供了若干用于通用 WebAssembly 互操作的实验性注解。

@WasmImport 和 @WasmExport 分别让你调用定义在 Kotlin/Wasm 模块之外的函数,以及把 Kotlin 函数暴露给宿主或其他 Wasm 模块。

由于这些机制仍在演进,所有注解都被标记为实验性。你必须显式选择启用才能使用它们,而且它们的设计或行为在未来的 Kotlin 版本中可能发生变化。

调试期间的重新加载

在现代浏览器中调试应用开箱即用。当你运行开发用的 Gradle 任务(*DevRun)时,Kotlin 会自动把源文件提供给浏览器。

不过,默认提供源码可能会导致在 Kotlin 编译和打包完成之前,应用在浏览器中反复重新加载。作为变通办法,请调整 webpack 配置以忽略 Kotlin 源文件,并禁止监视所提供的静态文件。在项目根目录的 webpack.config.d 目录中添加一个内容如下的 .js 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
config.watchOptions = config.watchOptions || {
    ignored: ["**/*.kt", "**/node_modules"]
}

if (config.devServer) {
    config.devServer.static = config.devServer.static.map(file => {
        if (typeof file === "string") {
            return {
                directory: file,
                watch: false,
            }
        } else {
            return file
        }
    })
}