Dirty Deeds Done Dart Cheap, experiments with dart2wasm

TheComputerMTheComputerM
4 min read

As developers, we all love carefully crafted packages and API wrappers that make the process of programming more streamlined, yet there is a certain beauty to patching together something hacky with some bandages and seeing it work.

FFIgenPad was one such project, simply put I had to use the experimental dart2wasm compiler to build ffigen to wasm using experimental ffi bindings with libclang.wasm, which were generated with an experimental option using ffigen.

Anyways let's get into the fun stuff

ABI specific integers in dart2wasm

Simply put, we can't use ABI specific integers in dart:ffi bindings, instead I had to convert it to a type with a specified size which wasn't that hard using ffigen.

type-map:
  "native-types":
    "char":
      "lib": "ffi"
      "c-type": "Uint8"
      "dart-type": "int"
    "unsigned int":
      "lib": "ffi"
      "c-type": "Uint32"
      "dart-type": "int"
    "int":
      "lib": "ffi"
      "c-type": "Int32"
      "dart-type": "int"
    "long long":
      "lib": "ffi"
      "c-type": "Int64"
      "dart-type": "int"
    "unsigned long long":
      "lib": "ffi"
      "c-type": "Uint64"
      "dart-type": "int"

This basically converts types like int to an int32.


Structs support in dart2wasm

We can't define ffi.Struct using dart:ffi when the target is WebAssembly, instead I used wrapper functions written in C that take in pointers as arguments and outputs instead of structs, and made all the structs be typed as ffi.Opaque.

long long clang_Type_getAlignOf_wrap(CXType *cxtype) {
  return clang_Type_getAlignOf(*cxtype);
}
final class CXUnsavedFile extends ffi.Opaque {}

Incomplete support for dart:typed_data

You might have noticed that I type casted Char as an Uint8 instead of Utf8 provided by package:ffi, that is because package:ffi uses dart:typed_data to convert its Utf8 to a dart string.

extension Utf8Pointer on Pointer<Utf8> {
  // ...
  String toDartString({int? length}) {
    // ...
    //       this function right here ⤵
    return utf8.decode(codeUnits.asTypedList(length));
  }

asTypedList isn't properly implemented when compiling to WASM there I have to write my own extension on Pointer<Uint8> (although in hindsight I still could have used Utf8 and overrode the extension).


Allocating memory

The malloc and calloc functions provided by package:ffi don't really play nicely with WASM memory, instead I had to export the malloc and free functions using emscripten to write my own Allocator instance.

import 'dart:ffi' as ffi;

@ffi.Native<ffi.Pointer<ffi.Void> Function(ffi.Int)>(symbol: 'malloc')
external ffi.Pointer<ffi.Void> _wasmAllocate(
  int size,
);

@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(symbol: 'free')
external void _wasmDeallocate(
  ffi.Pointer<ffi.Void> ptr,
);

class _WasmAllocator implements ffi.Allocator {
  @override
  ffi.Pointer<T> allocate<T extends ffi.NativeType>(
    int byteCount, {
    int? alignment,
  }) {
    return _wasmAllocate(byteCount).cast<T>();
  }

  @override
  void free(ffi.Pointer<ffi.NativeType> pointer) {
    _wasmDeallocate(pointer.cast());
  }
}

Adding functions to memory

The way to pass functions across boundaries is by using NativeCallable.isolateLocal, unfortunately for us, dart2wasm doesn't support this. Instead, we use addFunction as provided by emscripten that returns a function pointer, which we then pass to a wrapper function.

int visitorWrapper(int childAddress, int parentAddress, int _) {
  final child = clang_types.CXCursor.fromAddress(childAddress);
  final parent = clang_types.CXCursor.fromAddress(parentAddress);
  return callback(child, parent);
}
final visitorIndex = addFunction(visitorWrapper.toJS, 'iiii');
final result = clang.clang_visitChildren(this, visitorIndex);

typedef enum CXChildVisitResult (*ModifiedCXCursorVisitor)(
    CXCursor *cursor, CXCursor *parent, CXClientData client_data);

enum CXChildVisitResult _visitorwrap(CXCursor cursor, CXCursor parent,
                                     CXClientData clientData) {
  uintptr_t loc = *((uintptr_t *) clientData);
  ModifiedCXCursorVisitor visitor = (ModifiedCXCursorVisitor) loc;
  return visitor(&cursor, &parent, NULL);
}

unsigned clang_visitChildren_wrap(CXCursor *parent, uintptr_t _modifiedVisitor) {
  return clang_visitChildren(*parent, _visitorwrap, &_modifiedVisitor);
}

Filesystem

Obviously, I can't really use FS functions in code that will run isolated on the browser, or can I....

Emscripten provides a memory-based filesystem (called MEMFS) which we can use in conjunction with IOOverrides to mock the filesystem.

@JS('FS')
external MemFS get memfs;

class MemFSDirectory implements Directory {
    // ...
}

class MemFSFile implements File {
    // ...
}

Schrödinger's ffi.Pointer

Spoiler alert: the underlying type of a pointer is a boolean, example if you print pointer.runtimeType it will log boolean instead of something like a pointer class. That was the reason I couldn't add elements to a List of Pointer types, instead I used a list of integers which were the addresses of the pointers and recreated the pointers when I needed to access them.

List<clang_types.CXTranslationUnit> tuList = [];
tuList.add(tu) // throws error

type 'bool' is not a subtype of type 'Pointer' of 'value'

at GrowableList.add (wasm://wasm/009b4542:wasm-function[374]:0xb8fe5)


Congratulations, you made it to the end of the article, hope you enjoyed the journey. Personally, it was more fun rather than annoying to figure out workarounds, primarily because of Prerak Mann, Daco Harkes and Jackson Gardner chiming in whenever I asked for help.

0
Subscribe to my newsletter

Read articles from TheComputerM directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

TheComputerM
TheComputerM