wasm_ffi 2.2.0
wasm_ffi: ^2.2.0 copied to clipboard
A wrapper for wasm which can be used as a drop-in replacement for dart:ffi for Web platform.
wasm_ffi #
wasm_ffi intends to be a drop-in replacement for dart:ffi on the web platform using wasm. wasm_ffi is built on top of web_ffi.
The general idea is to expose an API that is compatible with dart:ffi but translates all calls through dart:js to a browser running WebAssembly.
Wasm with js helper as well as standalone wasm is supported. For testing emcc is used.
To simplify the usage, universal_ffi is provided, which uses wasm_ffi on web and dart:ffi on other platforms.
Differences to dart:ffi #
While wasm_ffi tries to mimic the dart:ffi API as close as possible, there are some differences. The list below documents the most importent ones, make sure to read it. For more insight, take a look at the API documentation.
- The
DynamicLibraryopenmethod is asynchronous. It also accepts some additional optional parameters. - If more than one library is loaded, the memory will continue to refer to the first library. This breaks calls to later loaded libraries! One workaround is to specify the correct library.allocator for each usage of
using. - Each library has its own memory, so objects cannot be shared between libraries.
- Some advanced types are still unsupported.
- There are some classes and functions that are present in
wasm_ffibut not indart:ffi; such things are annotated with@extra. - There is a new class
Memorywhich is IMPORTANT and explained in deepth below. - If you extend the
Opaqueclass, you must register the extended class using@extra registerOpaqueType<T>()before using it! Also, your class MUST NOT have type arguments (what should not be a problem). - There are some rules concerning interacting with native functions, as listed below.
Rules for functions #
There are some rules and things to notice when working with functions:
- When looking up a function using
DynamicLibrary.lookup<NativeFunction<NF>>()(orDynamicLibraryExtension.lookupFunction<T extends Function, F extends Function>()) the actuall type argumentNF(orTrespectively) of is not used: There is no type checking, if the function exported fromWebAssemblyhas the same signature or amount of parameters, only the name is looked up. - There are special constraints on the return type (not on parameter types) of functions
DF(orF) if you callNativeFunctionPointer.asFunction<DF>()(orDynamicLibraryExtension.lookupFunction<T extends Function, F extends Function>()what uses the former internally):- You may nest the pointer type up to two times but not more:
- e.g.
Pointer<Int32>andPointer<Pointer<Int32>>are allowed butPointer<Pointer<Pointer<Int32>>>is not.
- e.g.
- If the return type is
Pointer<NativeFunction>you MUST usePointer<NativeFunction<dynamic>>, everything else will fail. You can restore the type arguments afterwards yourself using casting. On the other hand, as stated above, type arguments forNativeFunctions are just ignored anyway. - To concretize the things above, the Appendix lists what may be used as return type, everyhing else will cause a runtime error.
- WORKAROUND: If you need something else (e.g.
Pointer<Pointer<Pointer<Double>>>), usePointer<IntPtr>and cast it yourselfe afterwards usingPointer.cast().
- You may nest the pointer type up to two times but not more:
Memory #
NOTE: While most of this section is still correct, some of it is now automated.
The first call you sould do when you want to use wasm_ffi is Memory.init(). It has an optional parameter where you can adjust your pointer size. The argument defaults to 4 to represent 32bit pointers, if you use wasm64, call Memory.init(8).
Contraty to dart:ffi where the dart process shares all the memory, on WebAssembly, each instance is bound to a WebAssembly.Memory object. For now, we assume that every WebAssembly module you use has it's own memory. If you think we should change that, open a issue on GitHub and report your usecase.
Every pointer you use is bound to a memory object. This memory object is accessible using the @extra Pointer.boundMemory field. If you want to create a Pointer using the Pointer.fromAddress() constructor, you may notice the optional bindTo parameter. Since each pointer must be bound to a memory object, you can explicitly speficy a memory object here. To match the dart:ffi API, the bindTo parameter is optional. Because it is optional, there has to be a fallback mechanism if no bindTo is specified: The static Memory.global field. If that field is also not set, an exception is thrown when invoking the Pointer.fromAddress() constructor.
Also, each DynamicLibrary is bound to a memory object, which is again accessible with @extra DynamicLibrary.boundMemory. This might come in handy, since Memory implements the Allocator class.
Usage Guide #
This guide covers how to build your WASM modules, generate bindings, and use them in both vanilla Dart and Flutter applications.
1. Building WASM with Emscripten #
You can compile your C/C++ code to WebAssembly using Emscripten. There are two main modes: with JavaScript glue code (recommended for most web apps) and standalone WASM.
Prerequisite
- Install Emscripten: Download Guide.
- Ensure
emccis in your PATH.
Option A: Emscripten WASM (with JavaScript glue)
Best for web apps needing JS interop.
emcc -o output.js input.c \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="MyModule"' \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_RUNTIME_METHODS=HEAPU8 \
-s EXPORTED_FUNCTIONS=["_myFunction", "_malloc", "_free"]
Key Flags:
-s MODULARIZE=1: Wraps code in a module.-s EXPORTED_RUNTIME_METHODS=HEAPU8: Required forwasm_ffito access memory.-s EXPORTED_FUNCTIONS: List all C functions to export (prefix with_). Always include_mallocand_free.
Option B: Standalone WASM
Best for environments with direct WASM support.
emcc -o output.wasm input.c \
-s STANDALONE_WASM=1 \
-s EXPORTED_FUNCTIONS=["_myFunction", "_malloc", "_free"]
Notes
- C++ Support: Explicitly export
__wasm_call_ctorsto ensure static constructors run:-Wl,--export=__wasm_call_ctors. - Optimization: Use
-Ozfor size,-O3for speed.
2. Generating Bindings with ffigen #
You can use ffigen to generate bindings, but you need a proxy to handle the difference between dart:ffi and wasm_ffi.
-
Create a Proxy File (
lib/src/proxy_ffi.dart):export 'package:wasm_ffi/wasm_ffi.dart' if (dart.library.ffi) 'dart:ffi'; -
Generate Bindings: Configure
ffigento generate bindings as usual. -
Update Generated File: Open the generated binding file and replace:
import 'dart:ffi' as ffi;with:
import 'proxy_ffi.dart' as ffi;Note: You can automate this with a simple script.
3. Usage in Vanilla Dart #
For a pure Dart web application:
-
Compile WASM: Use Option A (with JS glue) to get
libexample.jsandlibexample.wasm. -
HTML Setup: Include the JS glue in your
index.html.<script src="libexample.js"></script> -
Dart Code:
import 'package:wasm_ffi/wasm_ffi.dart'; import 'package:wasm_ffi/wasm_ffi_modules.dart'; void main() async { // Initialize Memory Memory.init(); // Load Module (processes the script tag) var module = await EmscriptenModule.process('MyModule'); var dylib = DynamicLibrary.fromModule(module); // Use dylib to look up functions or use generated bindings // ... }
4. Usage in Flutter #
For Flutter Web applications:
-
Assets: Place
libexample.jsandlibexample.wasmin yourassets/folder and add them topubspec.yaml. -
Dependency: Add
inject_jsto yourpubspec.yamlto help load the JS. -
Initialization:
import 'package:flutter/services.dart'; import 'package:inject_js/inject_js.dart' as Js; import 'package:wasm_ffi/wasm_ffi.dart'; import 'package:wasm_ffi/wasm_ffi_modules.dart'; Future<void> init() async { Memory.init(); // 1. Inject JS Glue await Js.importLibrary('assets/libexample.js'); // 2. Load WASM Binary var wasmFile = await rootBundle.load('assets/libexample.wasm'); var wasmMap = {'wasmBinary': wasmFile.buffer.asUint8List()}; // 3. Compile & Instantiate var module = await EmscriptenModule.compile(wasmMap, 'MyModule'); var dylib = DynamicLibrary.fromModule(module); }
5. Cross-Platform Support (Web + Native) #
To support both Web (via wasm_ffi) and Native (via dart:ffi) in the same codebase:
-
Proxy File: Enhance your
proxy_ffi.dartto conditionally export initialization logic.export 'package:wasm_ffi/wasm_ffi.dart' if (dart.library.ffi) 'dart:ffi'; export 'init_web.dart' if (dart.library.ffi) 'init_native.dart'; -
Init Files:
init_web.dart: ImplementsinitFfi()usingwasm_ffi(as shown in the Flutter/Vanilla sections).init_native.dart: ImplementsinitFfi()as a no-op or native setup.
-
Main Code:
import 'proxy_ffi.dart'; void main() async { await initFfi(); // ... use your bindings }
Appendix: Return Types #
Allowed return types for functions used as type parameter in NativeFunctionPointer.asFunction<DF>() and DynamicLibraryExtension.lookupFunction<T extends Function, F extends Function>():
intdoubleboolvoidPointer<Float>,Pointer<Pointer<Float>>Pointer<Double>,Pointer<Pointer<Double>>Pointer<Int8>,Pointer<Pointer<Int8>>Pointer<Uint8>,Pointer<Pointer<Uint8>>Pointer<Int16>,Pointer<Pointer<Int16>>Pointer<Uint16>,Pointer<Pointer<Uint16>>Pointer<Int32>,Pointer<Pointer<Int32>>Pointer<Uint32>,Pointer<Pointer<Uint32>>Pointer<Int64>,Pointer<Pointer<Int64>>Pointer<Uint64>,Pointer<Pointer<Uint64>>Pointer<IntPtr>,Pointer<Pointer<IntPtr>>Pointer<Opaque>,Pointer<Pointer<Opaque>>Pointer<Void>,Pointer<Pointer<Void>>Pointer<NativeFunction<dynamic>>,Pointer<Pointer<NativeFunction<dynamic>>>Pointer<MyOpaque>,Pointer<Pointer<MyOpaque>>whereMyOpaqueis a class extendingOpaqueand was registered before usingregisterOpaqueType<MyOpaque>()
Contributions are welcome! 🚀