webassembly 0.1.1
webassembly: ^0.1.1 copied to clipboard
WebAssembly for Dart and Flutter: one API that runs on wasmtime on native platforms and on the browser's own WebAssembly on the web.
webassembly #
WebAssembly for Dart and Flutter, on one API for every platform.
- on native platforms - Android, iOS, Linux, macOS, Windows - it runs on wasmtime, which this package compiles from source as part of your build;
- on the web it runs on the browser's own
WebAssembly.
Which engine you get is decided at compile time by a conditional import, so the code you write is the same either way.
The unified API #
import 'package:webassembly/webassembly.dart';
final module = await WebAssembly.compile(wasmBytes);
final instance = await WebAssembly.instantiate(module, {
'env': {'log': (int value) => print(value)},
});
print((instance.exports['add'] as Function)(20, 22));
print(instance.exports['mem']! as Memory);
WebAssembly, Module, Instance, Memory, Table, Global,
CompileError, LinkError and RuntimeError follow the JavaScript API of the
same names. test/unified_api_test.dart is one platform neutral file - it
embeds its .wasm fixtures instead of reading files - and is run against both
engines:
dart test test/unified_api_test.dart # wasmtime
dart test -p chrome test/unified_api_test.dart # a real browser
Entry points #
| Import | Contents |
|---|---|
package:webassembly/webassembly.dart |
the unified API; the only import a portable application needs |
package:webassembly/wasmtime.dart |
the wasmtime API: engines, stores, linkers, the type model, host function trampolines, configuration, fuel, module serialisation |
package:webassembly/parallel.dart |
WasmWorker and WasmPool: run WebAssembly off the calling isolate, or on several threads |
package:webassembly/raw.dart |
the generated C bindings, unguarded |
Everything except webassembly.dart is native only and needs its own import -
by design, since wasmtime, isolates and the C ABI do not exist on the web.
What differs on the web #
| native | web | |
|---|---|---|
i64 values |
int |
BigInt (JavaScript has no 64 bit integers) |
| module serialisation | Module.serialize in wasmtime.dart |
not available |
| engines, configuration, fuel | wasmtime.dart |
not available |
WasmWorker, WasmPool |
isolates | not available |
| errors | CompileError / LinkError / RuntimeError |
the same, mapped from the browser's |
The web backend maps the browser's linking failures onto LinkError, which is
what the specification and wasmtime report, so both engines hand you the same
error types.
Requirements #
- a Dart/Flutter SDK with build hooks and code assets - verified with Dart 3.13.5 and Flutter 3.47.7;
- Rust -
cargohas to be onPATH; wasmtime is a Rust library, and only cargo can compile it; - CMake and a C toolchain - on Windows, Visual Studio with Desktop
development with C++ (the hook applies
vcvarsitself). CMake must also be reachable fromPATHwhile cargo runs, because wasmtime's own C API build script shells out to it;vcvarsprovides it; - git - the first native build downloads the pinned wasmtime revision.
The first native build downloads about 100 MB of sources and compiles wasmtime, which takes a while; every build after that is incremental. A web build does no native work at all.
Where wasmtime comes from #
The wasmtime sources are not part of the published package. The build hook resolves them in this order:
-
hooks.user_definesfrom yourpubspec.yaml:hooks: user_defines: webassembly: wasmtime_source_dir: /path/to/a/wasmtime/checkout -
$WASMTIME_SOURCE_DIR. Build hooks run in a semi-hermetic environment that strips almost every environment variable, so this one only applies when the hook is invoked directly; inside a Flutter or Dart build use the user define above. -
the
wasmtime/git submodule, which is how this package is developed; -
otherwise a shallow download of one pinned revision into the per-user cache (
%LOCALAPPDATA%\dart_webassemblyon Windows,~/Library/Caches/...on macOS,$XDG_CACHE_HOMEelsewhere), where later builds find it again.
Notes:
- A checkout is whatever
git cloneofhttps://github.com/bytecodealliance/wasmtimeat the revision inhook/src/wasmtime_sources.dartgives you. - Leave
CARGO_TARGET_DIRunset. Upstream's CMake lets cargo choose its own target directory but derives the path it installs from from$CARGO_TARGET_DIR, which do not always agree.
Published path #
A published copy has no wasmtime/ directory, so the build hook downloads the
pinned revision and compiles that. Both halves can be checked locally:
# The download itself (~100 MB, about 15 s). CI runs this on every push.
WASMTIME_DOWNLOAD_TEST=1 dart test test/wasmtime_sources_test.dart
# A full build from a downloaded checkout rather than the submodule: point the
# user define above at one, drop the hook cache so the hook runs again, and
# build. This is also what the Windows job in CI does not cover, which is why
# the slower parts are worth running by hand before a release.
rm -rf example/.dart_tool/hooks_runner
cd example && flutter build windows
On Windows, $env:WASMTIME_DOWNLOAD_TEST = '1' and Remove-Item example\.dart_tool\hooks_runner -Recurse -Force.
Using wasmtime directly #
import 'package:webassembly/wasmtime.dart';
final engine = WasmEngine.configured((config) {
config.strategy = WasmStrategy.cranelift;
config.optLevel = WasmOptLevel.speed;
});
final module = WasmModule.compile(engine, wasmBytes);
final store = WasmStore(engine);
final instance = WasmLinker(engine).instantiate(store, module);
print((instance.export('add')! as WasmFunc).call([20, 22]));
Running off the calling isolate #
import 'package:webassembly/parallel.dart';
final worker = await WasmWorker.spawn(); // its own engine and store
final module = await worker.compile(wasmBytes);
final instance = await module.instantiate();
print(await instance.invoke('add', [20, 22])); // never blocks the caller
await worker.dispose();
// ... or spread the work over every processor:
final pool = await WasmPool.spawn(workers: 4);
final pooled = await (await pool.load(wasmBytes)).instantiate();
print(await pooled.callAll('work', [[1], [2], [3], [4]]));
await pool.dispose();
Regenerating the bindings #
ffigen parses the headers of the wasmtime checkout, which need the generated
wasmtime/conf.h - the file that says which features exist. Configuring the
project writes it to build/ffigen/include/:
cmake -S src -B build/ffigen-conf
dart run ffigen --config ffigen.yaml
Development #
dart test # wasmtime, workers and pools
dart test -p chrome test/unified_api_test.dart # the same API in a browser
cd example && flutter build windows # bundles wasmtime.dll
cd example && flutter build web # uses the browser instead
Cargo writes to the checkout's own target/ directory (for the submodule,
wasmtime/target/, which the submodule ignores).
License #
Apache-2.0; see LICENSE.
wasmtime itself is licensed under Apache-2.0 with LLVM exceptions. It is downloaded and compiled at build time, and the resulting shared library is redistributed inside your application, so keep wasmtime's license in mind when you ship.
Layout #
| Path | Purpose |
|---|---|
lib/webassembly.dart |
the unified API: the browser shaped WebAssembly |
lib/src/api/webassembly.dart |
that API, free of any backend |
lib/src/api/backend_io.dart |
its wasmtime backend |
lib/src/api/backend_web.dart |
its browser backend (dart:js_interop) |
lib/src/ffi/ |
ownership, marshalling, errors and values over the raw bindings |
lib/src/wasmtime/ |
the wasmtime engine, store, linker, module and handle API |
lib/src/parallel/ |
WasmWorker and WasmPool |
lib/src/wasmtime_bindings_generated.dart |
the generated C bindings (do not edit) |
hook/build.dart |
the build hook Flutter and Dart run |
lib/src/toolchain/wasmtime_sources.dart |
where the wasmtime sources come from (in lib/, because a package may only contain the hook entry points) |
src/CMakeLists.txt |
selects the cargo features and builds the C API as a shared library |
ffigen.yaml |
ffigen configuration |
example/ |
a Flutter app that builds for Windows and for the web |
wasmtime/ |
the git submodule, for development only |