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.
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
DynamicLibrary
open
method 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_ffi
but not indart:ffi
; such things are annotated with@extra
. - There is a new class
Memory
which is IMPORTANT and explained in deepth below. - If you extend the
Opaque
class, 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.
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
(orT
respectively) of is not used: There is no type checking, if the function exported fromWebAssembly
has 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 forNativeFunction
s are just ignored anyway. - To concretize the things above, return_types.md 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:
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.
dart pub add wasm_ffi
or
flutter pub add wasm_ffi
import 'package:wasm_ffi/ffi.dart' as ffi;
Future<void> main() async {
final library = await DynamicLibrary.open('path to wasm or js'); // NOTE: It is async
final func = library.lookupFunction<int Function(), int Function()>('functionName');
print(func());
}
Generates ffi bindings using package:ffigen
on the header file.
In the generated bindings file, replace import 'dart:ffi' as ffi;
with import 'package:wasm_ffi/ffi.dart' as ffi;
import 'package:wasm_ffi/ffi.dart';
import 'package:wasm_ffi/ffi_utils.dart';
import 'native_example_bindings.dart';
...
final library = await DynamicLibrary.open(libName);
final bindings = NativeExampleBindings(library);
// assuming that native library is has a function `hello` which takes a name and returns a string `Hello name!`
using((Arena arena) {
final cString = name.toNativeUtf8(allocator: arena).cast<Char>();
return bindings.hello(cString).cast<Utf8>().toDartString();
}, library.allocator); // library.allocator is optional if only one module is loaded
...
The generated wasm file needs all exported function. To ensure that, one of the two can be done:
- Use EMSCRIPTEN_KEEPALIVE annotation on all exported functions
- Define EXPORTED_FUNCTIONS when compiling the wasm
Contributions are welcome! 🚀