Kotlin Multiplatform APIs for loading and calling WASI WebAssembly plugins
中文文档 · English · Documentation
Wasmline is a Kotlin Multiplatform library for loading and calling WASI-compliant WebAssembly plugins in Android, iOS, Desktop, and Web applications. Native execution uses the Wasmtime fork distributed by Wasmline; browser execution uses the platform WebAssembly API.
Define the service contract (commonMain):
// shared/src/commonMain/kotlin/com/example/EchoService.kt
import crow.wasmline.WasmlineService
interface EchoService : WasmlineService {
fun echo(message: String): String
}Register the implementation in the plugin (wasmWasiMain):
// plugin/src/wasmWasiMain/kotlin/Main.kt
import crow.wasmline.Wasmline
import crow.wasmline.bind
fun main() {
Wasmline.get().bind(object : EchoService {
override fun echo(message: String): String {
return "Response from WASI plugin: $message"
}
})
}Load the plugin and invoke services from the host:
import crow.wasmline.WasmlineConfig
import crow.wasmline.WasmlineLoadResult
import crow.wasmline.link
import crow.wasmline.loader.WasmlineLoader
import crow.wasmline.loader.WasmlineLoadOptions
import crow.wasmline.loader.WasmlineTrustedKeySet
import crow.wasmline.network.ktor.KtorNetworkClient
suspend fun main() {
// Local paths and remote URLs are both supported — http(s):// loads the remote manifest
val module = when (val result = WasmlineLoader.load(
source = "https://example.com/plugin/manifest.wlm",
options = WasmlineLoadOptions(
runtimeConfig = WasmlineConfig(),
networkClient = KtorNetworkClient(),
trustedKeys = WasmlineTrustedKeySet.Builder()
.addHex(
algorithm = "Ed25519",
keyId = "release",
publicKeyHex = "5a778289bee0c57b05a1c48c8ef312da6ce8e4e4f13fc1a2e8e5aa4cde7ae0db",
)
.build(),
),
)) {
is WasmlineLoadResult.Success -> result.wasmline
is WasmlineLoadResult.Failure -> error(result.failure.message)
}
val response = module.link<EchoService>().echo("ping")
module.close()
}For a remote artifact, the loader streams bytes into the cache, verifies the SHA-256 digest, then publishes the file at its digest path. Set WasmlineLoadOptions.maxCacheBytes to change the default capacity of 512 MiB.
WasmlineLoader.load is a suspending API. Local artifacts and local manifests do not require a network adapter. A remote manifest needs wasmline-network-ktor, wasmline-network-okhttp, or a custom resolver only when its fresh manifest or selected artifact is missing from the configured cache. The runtime never hardcodes an HTTP engine.
Each API has a separate role: WasmlineLoader resolves, verifies, selects, and loads artifacts; WasmlineRuntime provides process-wide preload, engine warm-up, runtime information, and shutdown; each loaded Wasmline is an independently closeable artifact handle. Loading is lazy, so applications do not need an explicit runtime initialization call before WasmlineLoader.load().
Repeated loads of the same artifact have independent handles and service bindings; closing one does not close another. close() is idempotent. A handle reuses its CoreWasmModule facade, closes its sessions, and updates their isClosed state when it closes. Closing a session releases its registration without waiting for the module to close.
Note
link<T>() and bind(impl) are rewrite targets of the Kotlin IR compiler plugin. If wasmline-kotlin-plugin is not applied to the compilation unit, these calls fail at runtime with an error explaining that the compiler plugin did not rewrite them.
Declare the targets and AOT generations to compile. Omitting aotCompatibility
compiles the generation of the Wasmline build-tool version:
import crow.wasmline.gradle.WasmtimeTarget
import crow.wasmline.plugin.core.aot.AotGenerations
wasmline {
wasmtime {
aotCompatibility {
generations(AotGenerations.Current)
}
targets = listOf(
WasmtimeTarget.PULLEY_64,
WasmtimeTarget.X86_64_LINUX,
WasmtimeTarget.X86_64_WINDOWS,
)
autoDownload.set(true)
}
}AotGenerations also provides Latest, Earliest, All, windowed forms such as
Current(before = 1, after = 1), and concrete version selection with
generations(version), generations(versions = listOf(...)), or
generations(min = ..., max = ...).
Selection rules, the packaged catalog fields, and the compatibility check are documented in Wasmtime Download and AOT.
The package stores artifacts by SHA-256 and keeps one manifest for every selected generation:
{pluginId}-{version}/
├── manifest.wlm
├── artifacts/sha256/{prefix}/{digest}.wasm|cwasm|pwasm
└── debug/
├── manifest.json
├── aot-build-record.json
└── artifact-index.json
Wasmline supports four explicit host-side invocation paths:
| Execution model | Invocation protocol | Input | Result |
|---|---|---|---|
CORE_WASM |
WASMLINE_SERVICE |
action name and byte payload | WasmlineCallResult<ByteArray> |
CORE_WASM |
RAW_EXPORT |
CoreWasmModule/CoreWasmSession numeric values, synchronous imports, and linear memory |
WasmlineCallResult<List<RawValue>> |
COMPONENT_MODEL |
WASMLINE_SERVICE |
action name and byte payload through wasmline.wit |
WasmlineCallResult<ByteArray> |
COMPONENT_MODEL |
COMPONENT_EXPORT |
declared Component Model values | WasmlineCallResult<WasmlineComponentCallResult> |
The runtime side of the Component Model path loads an already compiled component
binary. Optional plugin build steps can generate bindings and create that
binary from WIT through the Gradle plugin or the CLI; wasmline-plugin-core is
an internal module embedded in both tools, not a published artifact. The loader
itself does not run those tools. contractMetadata describes the
call contract when needed; it is not a WIT compiler input. See the
Component Service Protocol.
The browser runtime supports both Core Service and Core Raw Export paths. Web
uses raw .wasm, WebAssembly.Module/WebAssembly.Instance, synchronous
imports, and checked linear memory; native uses the Wasmtime bridge with
.cwasm/.pwasm AOT artifacts. Component Model typed calls remain native-only.
For RAW_EXPORT, load a CoreWasmModule, register synchronous RawImport
handlers before instantiate(), invoke RawValue exports, and use
RawMemory for bulk data. WasmlineWeb.registerBytes() is the browser path
for embedded .wasm; native selection remains AOT-only. Signed packages store
export signatures, imports, memory, and required features in
runtimeContract.rawAbi, not in free-form contractMetadata entries.
Core Wasmline calls return results instead of using exceptions for normal call failures:
import crow.wasmline.callResult
import crow.wasmline.invocation.WasmlineCallResult
import crow.wasmline.invocation.WasmlineErrorCode
when (val result = module.callResult("echo", payload)) {
is WasmlineCallResult.Success -> usePayload(result.value)
is WasmlineCallResult.Failure -> {
if (result.failure.code == WasmlineErrorCode.ACTION_NOT_BOUND) {
log("The plugin did not bind this action.")
}
log(result.failure.message)
}
}An unbound action returns ACTION_NOT_BOUND. It does not return an empty payload and does not crash the host. The same result-first rule applies to unknown actions, invalid payloads, traps, and handler failures. throwOnFailure() is an explicit adapter for code that chooses exception-style handling; it is not used by the result API by default.
WasmlineFailure is returned by result-based APIs for non-throwing failures,
WasmlineException is reserved for explicit throwing adapters, and
WasmlineLoadFailure describes failures before module creation. The
failure property is the only field that contains failure details in result
APIs.
The WASMLINE_SERVICE response frame starts with the four-byte WLMF magic marker and a one-byte frameVersion whose current value is 1. The magic marker identifies the frame format; it is not a security check. frameVersion identifies the response byte layout; it is not a Wasmtime, Kotlin, framework, or business API version. Raw Export and Component Model calls do not use this Core response frame.
The decoder rejects truncated frames and invalid UTF-8 messages with RESPONSE_MALFORMED. Legacy unframed responses remain supported: fewer than four bytes matching the WLMF prefix are preserved as payload; the full marker without a complete header is a malformed frame.
Load through manifest.wlm when the package is available. The Loader verifies the package and
copies its execution model, invocation protocol, target identity, artifact
format, and AOT compatibility profile into the selected descriptor. A direct,
caller-trusted AOT descriptor must provide all of those fields explicitly; a
path such as component.cwasm is not enough to prove compatibility.
| Platform | Architecture | Artifact Support | Loading |
|---|---|---|---|
| Android | v8a, x86_64 | .cwasm / .pwasm |
wasmtime |
| Android | v7a, x86 | .pwasm |
wasmtime |
| iOS | arm64 | .pwasm |
wasmtime |
| macOS | arm64 | .cwasm / .pwasm |
wasmtime |
| Linux | x86_64 | .cwasm / .pwasm |
wasmtime |
| Windows | x86_64 | .cwasm / .pwasm |
wasmtime |
| Web (Kotlin/JS · Kotlin/WasmJS) | Browser JS engine | .wasm |
web |
Native selection uses the AOT compatibility profile IDs reported by the linked engine. It does not infer compatibility from a Maven version or filename.
Warning
The minimum required Kotlin version is 2.4.0.
Earlier Kotlin/Native compilers may reject the published KLIB ABI; newer compiler versions require separate validation of the runtime artifacts and compiler plugin.
// settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
// build.gradle.kts
plugins {
id("io.github.crowforkotlin.wasmline") version "1.0.0-beta1"
}The plugin registers the packaging tasks and applies wasmline-kotlin-plugin,
which rewrites link<T>() and bind(impl).
Use the BOM on JVM and Android so the runtime, Loader, network adapter, and engine resolve to one strict Wasmline version:
dependencies {
implementation(platform("io.github.crowforkotlin:wasmline-bom:1.0.0-beta1"))
implementation("io.github.crowforkotlin:wasmline-loader")
implementation("io.github.crowforkotlin:wasmline-network-ktor")
implementation("io.github.crowforkotlin:wasmline-engine-cranelift")
}Kotlin Multiplatform source sets should use one shared version variable for all Wasmline coordinates; engine modules are not versioned independently.
See Installation for the full module table and per-platform setup.
Run in a project that applies id("io.github.crowforkotlin.wasmline"):
./gradlew wasmlineAssembleDebug # debug package for local testing
./gradlew wasmlineAssembleRelease # release package for distribution
./gradlew wasmlineServerDeploy # build and serve the configured packagewasmlineServerDeploy serves the DEBUG variant at http://localhost:8080.
Select another variant with a typed value:
import crow.wasmline.gradle.WasmlineBuildVariant
wasmline {
server {
deployVariant = WasmlineBuildVariant.RELEASE
}
}Run from the repository root. Each command uses the Gradle wrapper of the project it builds:
./gradlew assemble # modules and Android AARs
./gradlew :wasmline:jvmTest # JVM tests
(cd wasmline-plugin-test && ./gradlew jvmTest) # plugin integration tests
./gradlew :wasmline:jsBrowserProductionLibraryDistribution # JS distribution
./gradlew :wasmline:wasmJsBrowserProductionLibraryDistribution
./gradlew :wasmline:iosArm64Binaries :wasmline:iosSimulatorArm64Binaries # macOS host
(cd samples/multiplatform-app && ./gradlew :sample-apps:multiplatform:desktopApp:packageDistributionForCurrentOS)(cd samples/minimal-service && ./gradlew run) # minimal service
(cd samples/minimal-raw-export && ./gradlew run) # minimal raw export
(cd samples/multiplatform-app && ./gradlew :sample-apps:multiplatform:desktopApp:run)
(cd samples/multiplatform-app && ./gradlew :sample-apps:native:runKotlinNativeSample)./scripts/release.sh expected-tag prints the tag for the current manifest.
Publication runs only for a release tag; pushing to main never publishes.
See Releasing
for the tag format, publication checks, credentials, and artifact verification,
and Gradle plugin task reference
for the complete task set.
Wasmline is distributed under the Apache License, Version 2.0. See LICENSE for the complete license text.




