universal_ffi
universal_ffi is a wrapper on top of wasm_ffi and dart:ffi to provide a consistent API across all platforms.
It also has some helper methods to make it easier to use.
wasm_ffi has a few limitations, so some of the features of dart:ffi are not supported. Most notably:
- Array
- Struct
- Union
Usage
Install
dart pub add universal_ffi
or
flutter pub add universal_ffi
Generate binding files
Generates bindings using package:ffigen.
Replace import 'dart:ffi' as ffi; with import 'package:universal_ffi/ffi.dart' as ffi; in the generated binding files.
Using FfiHelper
import 'package:universal_ffi/ffi.dart';
import 'package:universal_ffi/ffi_helper.dart';
import 'package:universal_ffi/ffi_utils.dart';
import 'native_example_bindings.dart';
...
final ffiHelper = await FfiHelper.load('ModuleName');
final bindings = WasmFfiBindings(ffiHelper.library);
// use bindings
using((Arena arena) {
...
}, ffiHelper.library.allocator);
...
Features
DynamicLibrary.openAsync()
DynamicLibrary.open is synchronous for 'dart:ffi', but asynchronous for 'wasm_ffi'. This helper method uses both asynchronously.
FfiHelper.load()
FfiHelper.load resolves the modulePath to the platform specific path in a variety of ways.
Simple usage
In the case, it is assumed that all platforms load a shared library from the same relative path. For example, if the modulePath = 'path/name', then the following paths are used:
- Web: 'path/name.js' or 'path/name.wasm' (if
isStandaloneWasmoption is specified) - Linux & Android: 'path/name.so'
- Windows: 'path/name.dll'
- macOS & iOS: 'path/libname.dylib'
Option: isStaticallyLinked
If the modulePath = 'path/name' and isStaticallyLinked option is specified, then the following paths are used:
- Web: 'path/name.js' or 'path/name.wasm' (if
isStandaloneWasmoption is specified) - All other platforms: Instead of loading a shared library, calls DynamicLibrary.process().
Option: isFfiPlugin (used for Flutter Ffi Plugin)
If the modulePath = 'path/name' and isFfiPlugin option is specified, then 'path' is ignored and the following paths are used:
- Web: 'assets/packages/name/assets/name.js' or 'assets/packages/name/assets/name.wasm' (if
isStandaloneWasmoption is specified) - Linux & Android: 'name.so'
- Windows: 'name.dll'
- macOS & iOS: 'name.framework/name'
Overrides
Overrides can be used to specify the path to the module to be loaded for specific AppType. Override strings are used as is.
Multiple wasm_ffi modules in the same project
If you have multiple wasm_ffi modules in the same project, the global memory will refer only to the first loaded module. So unless the memory is explicitly specified, the memory from the first loaded module will be used for all modules, causing unexpected behavior. One option is to explicitly use library.allocator for wasm & malloc/calloc for ffi. Alternatively, you can use FfiHelper.safeUsing or FfiHelper.safeWithZoneArena:
FfiHelper.safeUsing()
FfiHelper.safeUsing is a wrapper for using. It ensures that the library-specific memory is used.
FfiHelper.safeWithZoneArena()
FfiHelper.safeWithZoneArena is a wrapper for withZoneArena. It ensures that the library-specific memory is used.
📦 Creating a Plugin
Naming the Plugin/Module
When creating a plugin, choice of moduleName is critical. It should be consistent across:
- Usage:
FfiHelper.load('moduleName'). - Files: Output filenames (e.g.,
libmoduleName.so,moduleName.dll,moduleName.wasm). - Emscripten: The
EXPORT_NAMEfor Emscripten builds. - Flutter Plugin: For Flutter FFI plugins, the
moduleNameMUST match the package name specified inpubspec.yaml. This enables correct asset resolution (assets/packages/moduleName/...).
Creating a Dart Plugin (see example)
To create a pure Dart plugin that works on all platforms (including web):
-
Project Structure:
src/: Contains your C/C++ source code.lib/: Dart code and generated bindings.web/assets/: Recommended location for compiled WASM/JS modules.assets/: Location for compiled dynamic libraries (optional).
-
Build Native Assets:
- Compile
src/to shared libraries (.so,.dylib,.dll) for desktop platforms. - Compile
src/to WASM/JS for web (see WASM Compilation).
- Compile
-
Usage:
final ffiHelper = await FfiHelper.load('web/assets/emscripten/moduleName'); // or just 'moduleName' if assets are in root logic
Creating a Flutter FFI Plugin (see example_ffi_plugin)
To create a Flutter FFI plugin that includes Web support:
-
Project Structure: Follow the standard Flutter FFI plugin structure.
src/: C/C++ source code.assets/: Place compiled WASM/JS modules here.
-
Build Native Assets:
- Mobile/Desktop: Use
CMakeLists.txtandpubspec.yamlas per standard Flutter plugin development. - Web: Manually compile WASM/JS assets and place them in
assets/.
- Mobile/Desktop: Use
-
pubspec.yaml: Ensure your
assets/folder is included:flutter: assets: - assets/ -
Usage:
// Must match package name final ffiHelper = await FfiHelper.load('moduleName', options: {LoadOption.isFfiPlugin});
WASM Compilation
To support Web, you must compile your C code using emcc (Emscripten). A plugin should support either Emscripten JS or Standalone WASM, not both simultaneously config-wise.
The examples illustrate both for completeness, but you should choose one.
Option 1: Emscripten JS (Recommended)
This generates a .js file that loads the .wasm file. This is generally more compatible.
Command:
emcc -o path/to/moduleName.js \
src/native_code.c \
-O3 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="moduleName"' \
-s ALLOW_MEMORY_GROWTH=1 \
-s ENVIRONMENT='web,worker' \
-s EXPORTED_RUNTIME_METHODS=HEAPU8 \
-s EXPORTED_FUNCTIONS='["_malloc", "_free", "_your_function"]'
Crucial: You MUST include -s EXPORTED_RUNTIME_METHODS=HEAPU8. This exports the memory object so universal_ffi can access it.
Option 2: Standalone WASM
This generates a single .wasm file without glue JS.
Command:
emcc -o path/to/moduleName.wasm \
src/native_code.c \
-O3 \
-s STANDALONE_WASM=1 \
-s ENVIRONMENT='web,worker' \
-s EXPORTED_FUNCTIONS='["_malloc", "_free", "_your_function"]'
Crucial: You MUST include --export=__wasm_call_ctors if you are using C++ to ensure static constructors run.
Naming Convention
- Emscripten JS: Output
moduleName.js(andmoduleName.wasmwill be generated next to it). - Standalone WASM: Output
moduleName.wasm.
Contributions are welcome! 🚀
Libraries
- ffi
- Foreign Function Interface for interoperability with the C programming language.
- ffi_helper
- Useful helpers for working with Foreign Function Interface (FFI).
- ffi_utils
- Utilities for working with Foreign Function Interface (FFI) code, incl. converting between Dart strings and C strings encoded with UTF-8 and UTF-16.