NOTE: In steps 4.2, 5 and 6 this tutorial does slightly different things for flutter and no-flutter. For the flutter files see example_flutter, for the no-flutter files see example_no_flutter. This tutorial assumes that your workdir is one of these two folders.
This tutorial assumes you have read web_ffi
's README.
We will walk through a simple example how we ported opus_dart to the web.
The proxy_ffi is a simple dart file inside your project. It will conditionally import web_ffi
or dart:ffi
based on your platform and export it agian. We will later extend this proxy.
Create lib/src/proxy_ffi.dart
:
export 'package:web_ffi/web_ffi.dart' if (dart.library.ffi) 'dart:ffi';
Binding code is used to expose the c code to dart, usually based on the headers in the include
folder of a c library. You can either write the code yourself or use tools like ffi_tool or ffigento generate it, based on the c headers. In the binding code, don't import dart:ffi
but the proxy file. If you use autogenerated binding code, you must change that manually after generation!
This is the binding code we want to use (generated by ffi_tool), saved in lib/src/generated.dart
:
/// Contains methods and structs from the opus_libinfo group of opus_defines.h.
///
/// AUTOMATICALLY GENERATED FILE. DO NOT MODIFY.
library opus_libinfo;
import 'proxy_ffi.dart' as ffi;
typedef _opus_get_version_string_C = ffi.Pointer<ffi.Uint8> Function();
typedef _opus_get_version_string_Dart = ffi.Pointer<ffi.Uint8> Function();
class FunctionsAndGlobals {
FunctionsAndGlobals(ffi.DynamicLibrary _dynamicLibrary)
: _opus_get_version_string = _dynamicLibrary.lookupFunction<
_opus_get_version_string_C, _opus_get_version_string_Dart>(
'opus_get_version_string',
);
/// Gets the libopus version string.
///
/// Applications may look for the substring "-fixed" in the version string to determine whether they have a fixed-point or floating-point build at runtime.
///
/// @returns Version string
ffi.Pointer<ffi.Uint8> opus_get_version_string() {
return _opus_get_version_string();
}
final _opus_get_version_string_Dart _opus_get_version_string;
}
We use emscripten to compile our c code to WebAssembly, and also generate the glue JavaScript we need.
It is important to pass the -s MODULARIZE=1
directive to emcc
and also give the module a name using -s EXPORT_NAME=libopus
. In the following, we assume our module name to be libopus
so everytime you read libopus
below, substitute it with your own module name! Futhermore it is important to tell emcc
to export all the c symbols you want to bind against. You can either speficy every symbol you want to export manually and explictly (see the emscripten documentation on how to do this) or pass the -s MAIN_MODULE=1
directive to export all symbols. You must make sure that the malloc
and the free
functions get exported (-s MAIN_MODULE=1
should take care of that automatically).
For a convenient build process, we use Docker. Here is the Dockerfile we used to build:
#Current version: 2.0.21
FROM emscripten/emsdk
RUN git clone --branch v1.3.1 https://github.com/xiph/opus.git
WORKDIR ./opus
RUN apt-get update \
&& DEBIAN_FRONTENTD="noninteractive" apt-get install -y --no-install-recommends \
autoconf \
libtool \
automake
ENV CFLAGS='-O3 -fPIC'
ENV CPPFLAGS='-O3 -fPIC'
RUN ./autogen.sh \
&& emconfigure ./configure \
--disable-intrinsics \
--disable-rtcd \
--disable-extra-programs \
--disable-doc \
--enable-static \
--disable-stack-protector \
--with-pic=ON \
&& emmake make
RUN mkdir emc_out \
&& emcc -O3 -s MAIN_MODULE=1 -s EXPORT_NAME=libopus -s MODULARIZE=1 ./.libs/libopus.a -o ./emc_out/libopus.js
WORKDIR ./emc_out
After this we have successfully compiled the opus c library and obtained libopus.js
and libopus.wasm
.
So now it's time to inizalize everything. Our goal is to obtain a DynamicLibrary
. Since that API for that is different for dart:ffi
and web_ffi
we need to write two files which will be conditionally imported.
This is the conventinal part. Create lib/src/init_ffi.dart
:
// Notice that in this file, we import dart:ffi and not proxy_ffi.dart
import 'dart:ffi';
// For dart:ffi platforms, this can be a no-op (empty function)
Future<void> initFfi() async {
// If you ONLY want to support web, uncomment this exception
// throw new UnsupportedError('This package is only usable on the web!');
}
DynamicLibrary openOpus() {
// If you ONLY want to support web, uncomment this exception
// throw new UnsupportedError('This package is only usable on the web!');
return new DynamicLibrary.open('libopus.so');
}
Before we can go on actually initalize our Module
in dart, we need to include libopus.wasm
and libopus.js
in our project. There are several ways to do that. First we will discuss the theoretical background. Than you can decide if you want to follow our sample to include the files or know a way that is more suited for your project.
web_ffi
will bind against your web runtime, access the global JavaScript object and expect to find a property named libopus
, which holds a function (the so called module-function
) that takes one argument (we will refer to this argument as arg0
). Usually, all this is already written in libopus.js
, so we need to include that file. If the module-function
is later called from inside dart, it will try to instantiate the actuall WebAssembly instance. Since we compiled with emscripten, the module-function
follows the standard emscripten approaches to try to get libopus.wasm
. This means it will firstly check if arg0['wasmBinary']
contains the bytes of libopus.wasm
, and if so, use them. If not it will try to access libopus.wasm
via http(s) from the same site libopus.js
is included in.
We don't want to alter the files flutter puts out after building, so we will include everyhing we need into the flutter app. For libopus.js
and libopus.wasm
, we include them as flutter assets, to later inject the JavaScript from the assets into the runtime, we use the inject_js plugin. Lastly, we will use the EmscriptenModule.compile()
function with bytes also loaded from the assets to use the arg0['wasmBinary']
approach. So we need to update our pubspec.yaml
:
name: web_ffi_example_flutter
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
inject_js: ^2.0.0
web_ffi:
path: ../..
flutter:
assets:
- assets/libopus.js
- assets/libopus.wasm
After running flutter packages get
we can write our init file at lib/src/init_web.dart
:
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:inject_js/inject_js.dart' as Js;
// Notice that in this file, we import web_ffi and not proxy_ffi.dart
import 'package:web_ffi/web_ffi.dart';
// and additionally
import 'package:web_ffi/web_ffi_modules.dart';
// Note that if you use assets included in a package rather them the main app,
// the _basePath would be different: 'packages/<package_name>/assets'
const String _basePath = 'assets';
Module? _module;
Future<void> initFfi() async {
// Only initalize if there is no module yet
if (_module == null) {
Memory.init();
// If your generated code would contain something that
// extends Opaque, you would register it here
// registerOpaqueType<MyOpaque>();
// Inject the JavaScript into our page
await Js.importLibrary('$_basePath/libopus.js');
// Load the WebAssembly binaries from assets
String path = '$_basePath/libopus.wasm';
Uint8List wasmBinaries = (await rootBundle.load(path)).buffer.asUint8List();
// After we loaded the wasm binaries and injected the js code
// into our webpage, we obtain a module
_module = await EmscriptenModule.compile(wasmBinaries, 'libopus');
}
}
DynamicLibrary openOpus() {
Module? m = _module;
if (m != null) {
return new DynamicLibrary.fromModule(m);
} else {
throw new StateError('You can not open opus before calling initFfi()!');
}
}
Usually, if we are not using flutter we use dart2js
what will output a JavaScript file (lets call this main.dart.js
), which will then be included using a <script>
tag into a website. We will simply put libopus.js
and libopus.wasm
in the same folder as main.dart.js
and add an extra script tag to our website to point to libopus.js
. Our sample website is in the web
folder, so we create web/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>web_ffi Test Page</title>
<script defer src="main.dart.js"></script>
<script src="libopus.js"></script>
</head>
<body>
</body>
</html>
Then we will use the EmscriptenModule.process()
function which will internally open the module-function
with an arg
without wasmBinary
set, so the emscripten logic will fetch libopus.wasm
automatically.
Here is our lib/src/init_web.dart
:
// Notice that in this file, we import web_ffi and not proxy_ffi.dart
import 'package:web_ffi/web_ffi.dart';
// and additionally
import 'package:web_ffi/web_ffi_modules.dart';
Module? _module;
Future<void> initFfi() async {
// Only initalize if there is no module yet
if (_module == null) {
Memory.init();
// If your generated code would contain something that
// extends Opaque, you would register it here
// registerOpaqueType<MyOpaque>();
// We use the process function here since we added
// libopus.js to our html with a <script> tag
_module = await EmscriptenModule.process('libopus');
}
}
DynamicLibrary openOpus() {
Module? m = _module;
if (m != null) {
return new DynamicLibrary.fromModule(m);
} else {
throw new StateError('You can not open opus before calling initFfi()!');
}
}
We now update the proxy to also export the correct init file. Change lib/src/proxy_ffi.dart
to look like:
export 'package:web_ffi/web_ffi.dart' if (dart.library.ffi) 'dart:ffi';
export 'init_web.dart' if (dart.library.ffi) 'init_ffi.dart';
Now we write our app code. The general scheme is to import proxy_ffi.dart
, and await it's initFfi()
. Then we can get a DynamicLibrary
using openOpus()
and instantiate our binding code with this DynamicLibrary
. Finally we use our binding code to call the WebAssembly. Again, the code for flutter and no-flutter is slightly different here.
A string in c is usually represented as a sqeuence of bytes and terminated with a 0 byte. The function we are about to call gives us a pointer to the first element in that sequence. To convert it to something usable in dart we will take that pointer, find the strings length by searching the next 0 byte, and use dart:convert
to convert it to dart.
In lib/src/c_strings.dart
:
import 'dart:convert';
import 'proxy_ffi.dart';
String fromCString(Pointer<Uint8> cString) {
int len = 0;
while (cString[len] != 0) {
len++;
}
return len > 0 ? ascii.decode(cString.asTypedList(len)) : '';
}
/// Don't forget to free the c string using the same allocator if your are done with it!
Pointer<Uint8> toCString(String dartString, Allocator allocator) {
List<int> bytes = ascii.encode(dartString);
Pointer<Uint8> cString = allocator.allocate<Uint8>(bytes.length);
cString.asTypedList(bytes.length).setAll(0, bytes);
return cString;
}
If we use flutter, we edit the main file in lib/main.dart
to look like this:
import 'package:flutter/material.dart';
import 'src/proxy_ffi.dart';
import 'src/c_strings.dart';
import 'src/generated.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initFfi();
DynamicLibrary dynLib = openOpus();
FunctionsAndGlobals opusLibinfo = FunctionsAndGlobals(dynLib);
String version = fromCString(opusLibinfo.opus_get_version_string());
runApp(MyApp(version));
}
class MyApp extends StatelessWidget {
final String _opusVersion;
const MyApp(this._opusVersion);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'web_ffi Demo',
home: Scaffold(
appBar: AppBar(
title: Text('web_ffi Demo'),
centerTitle: true,
),
body: Container(
alignment: Alignment.center,
child: Text(_opusVersion),
)),
);
}
}
First of all, to export the files in the lib
directory correctly, we create lib/example_no_flutter.dart
with this:
library example_no_flutter;
export 'src/proxy_ffi.dart';
export 'src/generated.dart';
export 'src/c_strings.dart';
If we do not use flutter, our main file is bin/main.dart
, and should look like this:
import 'package:web_ffi_example_no_flutter/example_no_flutter.dart';
Future<void> main() async {
await initFfi();
DynamicLibrary opus = openOpus();
FunctionsAndGlobals opusLibinfo = new FunctionsAndGlobals(opus);
Pointer<Uint8> cString = opusLibinfo.opus_get_version_string();
print(fromCString(cString));
}
With everyhing in place, it is time to run the app.
With flutter its straight forward, simply run
flutter run -d chrome
and chrome will open.
There we should see a text field reading libopus 1.3.1
.
We already setup most files we need in section 4.2.2. The only thing missing is main.dart.js
, so lets use dart2js
to create it:
dart2js ./bin/main.dart -o ./web/main.dart.js
Next, we need to serve the web directory over http. For that we use dhttpd, a simple in dart written webserver. To install it use
pub global activate dhttpd
Then we go into the web
directory
cd web
and run dhttpd from there
pub global run dhttpd -p 8080
We can now navigate to http://localhost:8080, and open the JavaScript console using the development-tools of the browser.
There we should see the opus version printed: libopus 1.3.1
.