Skip to content

Latest commit

 

History

History
357 lines (304 loc) · 15.3 KB

README.md

File metadata and controls

357 lines (304 loc) · 15.3 KB

How to use web_ffi

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.

1. Write a proxy_fii

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';

2. Write normal binding code

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;
}

3. Compile your C code for web

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.

4. Initalize everything

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.

4.1 For non-web platforms

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');
}

4.2 For web platforms

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.

4.2.1 With flutter

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()!');
  }
}

4.2.2 Without flutter

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()!');
  }
}

4.3 Update the proxy

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';

5. Write your Apps code

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.

5.0 EXCURSE: String from C Pointer

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;
}

5.1 With flutter

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),
          )),
    );
  }
}

5.2 Without flutter

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));
}

6. Run the app

With everyhing in place, it is time to run the app.

6.1 With flutter

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.

6.2 Without flutter

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.