wasm_ffi provides a dart:ffi-like API for WebAssembly modules on the web.
It supports Dart web applications compiled with dart2js or dart2wasm,
including standalone Wasm and Emscripten JavaScript glue. JavaScript BigInt
conversion is used for 64-bit values crossing the JS boundary.
To simplify the usage, universal_ffi is provided, which uses wasm_ffi on web and dart:ffi on other platforms.
While wasm_ffi tries to mimic the dart:ffi API closely, there are some
important differences:
- 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. - Each loaded library has a module-specific
Memoryandallocator. - If you extend the
Opaqueclass, you must register it withregisterOpaqueType<T>()before use. The class must not have type arguments. - There are some rules concerning interacting with native functions, as listed below.
There are some rules and things to notice when working with functions:
- When looking up a function using
DynamicLibrary.lookup<NativeFunction<NF>>()orDynamicLibrary.lookupFunction<T extends Function, F extends Function>(), the native type argument is not used to validate the exported signature. The caller must provide the correct name, signature, and arity. - There are special constraints on the return type (not on parameter types) of functions
DF(orF) if you callNativeFunctionPointer.asFunction<DF>()orDynamicLibrary.lookupFunction(which 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:
Each DynamicLibrary.open call creates or binds a module-specific memory
object. Contrary to dart:ffi, separately loaded WebAssembly modules do not
share memory, so their pointers cannot be mixed.
Every pointer is bound to a memory object. Use Pointer.fromAddress() with its optional bindTo argument when an address must be bound explicitly.
Use the DynamicLibrary.allocator
property for allocations passed to that library.
This guide covers how to build your WASM modules, generate bindings, and use them in both vanilla Dart and Flutter applications.
Load a standalone module by URL and invoke an exported function:
import 'package:wasm_ffi/ffi.dart';
typedef AddNative = Int32 Function(Int32, Int32);
typedef AddDart = int Function(int, int);
Future<void> main() async {
final library = await DynamicLibrary.open('assets/example.wasm');
final add = library.lookupFunction<AddNative, AddDart>('add');
print(add(2, 3));
await library.close();
}Use a generated Emscripten .js glue file in the same way. open infers the
module kind from the extension and fetches the asset asynchronously.
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.
- Install Emscripten: Download Guide.
- Ensure
emccis in your PATH.
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"]Crucial: You MUST include -s EXPORTED_RUNTIME_METHODS=HEAPU8. This exports the memory object so universal_ffi can access it.
Optimization: Use -Oz for size, -O3 for speed.
Best for environments with direct WASM support.
emcc -o output.wasm input.c \
-s STANDALONE_WASM=1 \
-s EXPORTED_FUNCTIONS=["_myFunction", "_malloc", "_free"]Crucial: You MUST include --export=__wasm_call_ctors if you are using C++ to ensure static constructors run.
Optimization: Use -Oz for size, -O3 for speed.
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 in the consuming package:
export 'package:wasm_ffi/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.
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/ffi.dart'; void main() async { final dylib = await DynamicLibrary.open('assets/example.js'); // Use dylib to look up functions or use generated bindings // ... }
For Flutter Web applications:
-
Assets: Place the generated
.jsand.wasmfiles in the Flutter asset directory and add them topubspec.yaml. -
Initialization:
import 'package:wasm_ffi/ffi.dart'; Future<void> init() async { final dylib = await DynamicLibrary.open('assets/libexample.js'); }
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/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 }
Run these commands from the repository root:
dart pub get
dart format --output=none --set-exit-if-changed .
dart analyze lib test
dart test
dart test --compiler=dart2wasm test/marshaller_signature_test.dart
dart compile wasm test/standalone_wasm_test.dart -o /tmp/standalone_wasm_test.wasm
dart pub publish --dry-runThe default test suite runs on Chrome because the implementation uses
web-only JavaScript interop. CI also runs flutter analyze, flutter build web, and flutter build web --wasm in example_flutter. The complete test
suite compiled as dart2wasm may exceed Chromium's WasmGC subtype-depth limit;
the repository therefore tests dart2wasm signature logic separately and does
not claim wasm64 runtime support.
See AGENTS.md for contributor workflow and
ARCHITECTURE.md for component boundaries.
Allowed return types for functions used as type parameter in NativeFunctionPointer.asFunction<DF>() and DynamicLibrary.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! 🚀