Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dart

Overview

The Dart target produces a pure-Dart FFI package that wraps the C ABI using dart:ffi. It opens the shared library with DynamicLibrary.open and resolves each symbol via lookupFunction. There’s no native compilation step or ffigen run required; the generated .dart file is ready to import.

What gets generated

FilePurpose
dart/lib/weaveffi.dartdart:ffi bindings: loader, typedefs, lookups, wrappers, struct/enum classes
dart/pubspec.yamlPackage metadata and package:ffi dependency
dart/README.mdBasic usage instructions

Type mapping

IDL typeDart typeNative FFI typeDart FFI type
i32intInt32int
u32intUint32int
i64intInt64int
f64doubleDoubledouble
i8intInt8int
i16intInt16int
u8intUint8int
u16intUint16int
u64intUint64int
f32doubleFloatdouble
boolboolInt32int
stringStringPointer<Utf8>Pointer<Utf8>
bytesList<int>Pointer<Uint8>Pointer<Uint8>
handleintInt64int
StructNameStructNamePointer<Void>Pointer<Void>
EnumName (plain)EnumNameInt32int
EnumName (rich)EnumNamePointer<Void>Pointer<Void>
T?T?same as inner typesame as inner type
[T]List<T>Pointer<Void>Pointer<Void>
{K: V}Map<K, V>Pointer<Void>Pointer<Void>
iter<T>Iterable<T>Pointer<Void>Pointer<Void>

Booleans cross as Int32 (0/1) and the wrapper converts both ways.

Example IDL → generated code

version: "0.4.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        doc: Type of contact
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        doc: A contact record
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

The loader auto-detects the platform:

DynamicLibrary _openLibrary() {
  // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a
  // specific build artifact regardless of its file name or location.
  final override = Platform.environment['WEAVEFFI_LIBRARY'];
  if (override != null && override.isNotEmpty) return DynamicLibrary.open(override);
  if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib');
  if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so');
  if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll');
  throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
}

final DynamicLibrary _lib = _openLibrary();

Enums become Dart enhanced enums:

/// Type of contact
enum ContactType {
  personal(0),
  work(1),
  other(2),
  ;
  const ContactType(this.value);
  final int value;
  static ContactType fromValue(int value) =>
      ContactType.values.firstWhere((e) => e.value == value);
}

Structs are wrapped in classes with a dispose() method and getter methods that call the C accessors:

/// A contact record
class Contact {
  final Pointer<Void> _handle;
  Contact._(this._handle);

  void dispose() { _weaveffiContactsContactDestroy(_handle); }

  String get name {
    final err = calloc<_WeaveFFIError>();
    try {
      final result = _weaveffiContactsContactGetName(_handle, err);
      _checkError(err);
      return result.toDartString();
    } finally {
      calloc.free(err);
    }
  }
}

Each function emits a native typedef, Dart typedef, lookup, and top-level wrapper:

typedef _NativeWeaveffiContactsCreateContact =
    Int64 Function(Pointer<Utf8>, Pointer<Utf8>, Int32, Pointer<_WeaveFFIError>);
typedef _DartWeaveffiContactsCreateContact =
    int Function(Pointer<Utf8>, Pointer<Utf8>, int, Pointer<_WeaveFFIError>);
final _weaveffiContactsCreateContact = _lib.lookupFunction<
    _NativeWeaveffiContactsCreateContact,
    _DartWeaveffiContactsCreateContact>('weaveffi_contacts_create_contact');

int createContact(String name, String? email, ContactType contactType) {
  final err = calloc<_WeaveFFIError>();
  final namePtr = name.toNativeUtf8();
  try {
    final result = _weaveffiContactsCreateContact(
        namePtr, email, contactType.value, err);
    _checkError(err);
    return result;
  } finally {
    calloc.free(namePtr);
    calloc.free(err);
  }
}

Rich (algebraic) enums

A rich (algebraic) enum is a sum type whose variants carry associated data. A plain C-style enum surfaces as a Dart enum and crosses as an Int32; a rich enum instead lowers to an opaque object handle, so the generator emits a wrapper class with the same ownership model as a struct wrapper, a Pointer<Void> freed by an explicit dispose().

For a Shape enum with variants Empty, Circle { radius: f64 }, Rectangle { width: f32, height: f32 }, and Labeled { label: string, count: u8 }, the generator emits a companion ShapeTag enum, one factory per variant, a tag getter that maps the discriminant back to ShapeTag, and a getter per payload field:

/// An algebraic shape (sum type with associated data)
enum ShapeTag {
  empty(0),
  circle(1),
  rectangle(2),
  labeled(3),
  ;
  const ShapeTag(this.value);
  final int value;

  static ShapeTag fromValue(int value) =>
      ShapeTag.values.firstWhere((e) => e.value == value);
}

/// An algebraic shape (sum type with associated data)
class Shape {
  final Pointer<Void> _handle;
  Shape._(this._handle);

  void dispose() {
    _weaveffiShapesShapeDestroy(_handle);
  }

  ShapeTag get tag =>
      ShapeTag.fromValue(_weaveffiShapesShapeTag(_handle));

  /// A circle with a radius
  factory Shape.circle(double radius) {
    final err = calloc<_WeaveFFIError>();
    try {
      final result = _weaveffiShapesShapeCircleNew(radius, err);
      _checkError(err);
      return Shape._(result);
    } finally {
      calloc.free(err);
    }
  }

  /// Radius in points
  double get circleRadius {
    final result = _weaveffiShapesShapeCircleGetRadius(_handle);
    return result;
  }

  int get labeledCount {
    final result = _weaveffiShapesShapeLabeledGetCount(_handle);
    return result;
  }
}

The rest of the surface follows the same shape: factories Shape.empty(), Shape.circle(radius), Shape.rectangle(width, height), and Shape.labeled(label, count); getters circleRadius, rectangleWidth, rectangleHeight, labeledLabel, and labeledCount. Each resolves a weaveffi_shapes_Shape_<Variant>_new / weaveffi_shapes_Shape_<Variant>_get_<field> symbol, and weaveffi_shapes_Shape_tag backs the tag getter.

Construct a couple of variants, read the tag and a field, then pass the wrapper to a top-level function:

final circle = Shape.circle(2.0);
final labeled = Shape.labeled('unit', 3);
try {
  if (circle.tag == ShapeTag.circle) {
    print(circle.circleRadius);        // 2.0
  }
  print(labeled.labeledCount);         // 3

  print(describe(circle));             // render via the C ABI
  final bigger = scale(circle, 3.0);   // returns a new Shape
  bigger.dispose();
} finally {
  circle.dispose();
  labeled.dispose();
}

Ownership: a Shape wraps a Pointer<Void> that you own; call dispose() (which invokes weaveffi_shapes_Shape_destroy) exactly as with struct wrappers. The Shape returned by scale is a separate handle you also dispose.

Build instructions

Standalone Dart:

  1. Generate the bindings:

    weaveffi generate api.yaml -o generated --target dart
    
  2. Build the Rust shared library:

    cargo build --release -p your_library
    
  3. Make the cdylib findable at runtime:

    • macOS: DYLD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart
    • Linux: LD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart
    • Windows: place weaveffi.dll next to the script or add its directory to PATH.

Flutter:

  1. Generate the bindings as above.

  2. Cross-compile the Rust cdylib for every Flutter target you support (aarch64-apple-ios, aarch64-linux-android, x86_64-apple-darwin, etc.).

  3. Reference the generated package from your app’s pubspec.yaml:

    dependencies:
      weaveffi:
        path: ../generated/dart
    
  4. Bundle the cdylib per platform:

    • iOS / macOS: ship a Framework or use a podspec.
    • Android: place .so files under android/src/main/jniLibs/{abi}/.
    • Linux / Windows: place next to the executable or on the library search path.

Memory and ownership

  • Strings: Dart String values are converted with toNativeUtf8(). The wrapper frees the resulting pointer in a finally block. Returned UTF-8 pointers are decoded with toDartString().

  • Structs: wrappers hold a Pointer<Void>. The dispose() method calls the corresponding _destroy C function. Always wrap usage in try/finally:

    final contact = getContact(id);
    try {
      print(contact.name);
    } finally {
      contact.dispose();
    }
    
  • Optionals: T? returns check the native pointer against nullptr before wrapping; absent struct optionals become null.

Callbacks and listeners

A callbacks: entry in the IDL defines the native function-pointer type; a listeners: entry generates a register/unregister pair around it. Registration wraps the Dart closure in a NativeCallable, hands its nativeFunction pointer to the C ABI, and returns the int subscription id the native side minted:

// Live listener trampolines by subscription id. Holding the
// NativeCallable here keeps its native thunk alive until unregistered.
final Map<int, NativeCallable> _listenerCallables = {};

/// Registers a OnMessage listener. Returns a subscription id for
/// unregisterMessageListener().
int registerMessageListener(void Function(String message) callback) {
  final callable =
      NativeCallable<_NativeCb_weaveffi_events_OnMessage_fn>.isolateLocal(
          (Pointer<Utf8> message, Pointer<Void> context) {
    callback(message == nullptr ? '' : message.toDartString());
  });
  final id = _weaveffiEventsRegisterMessageListener(callable.nativeFunction, nullptr);
  _listenerCallables[id] = callable;
  return id;
}

/// Unregisters a listener previously registered with registerMessageListener().
void unregisterMessageListener(int id) {
  _weaveffiEventsUnregisterMessageListener(id);
  _listenerCallables.remove(id)?.close();
}
  • Lifetime. The live NativeCallable is stored in _listenerCallables keyed by subscription id; that reference keeps the native thunk and the captured closure alive. Unregistering removes the entry and close()s the callable. The C void* context slot is unused (nullptr); the closure travels inside the callable, so no registry id needs to cross the boundary.
  • Threading. Listener trampolines are NativeCallable.isolateLocal, not .listener: WeaveFFI listeners fire synchronously on the thread calling the producer API (here, while sendMessage runs), and the argument pointers are only valid for that borrow window, so they are converted to Dart values inside the callback before the producer frees them. An isolateLocal callable may only be invoked on the owning isolate’s thread, so events are delivered during the isolate’s own calls into the library rather than queued to the event loop.
  • Isolate lifetime. The generated code never sets keepIsolateAlive = false, so the dart:ffi default applies: a registered listener keeps its isolate alive until it is unregistered.

Async support

Functions marked async: true return a Future<T> backed by the _async-suffixed C launcher. The completion callback is a NativeCallable.listener, which may be invoked from any native thread: the event is posted to the owning isolate’s event loop, where it completes the Completer:

Future<TaskResult> runTask(String name) {
  final completer = Completer<TaskResult>();
  final namePtr = name.toNativeUtf8();
  late NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_task> callable;
  callable = NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_task>.listener(
      (Pointer<Void> context, Pointer<_WeaveFFIError> err, Pointer<Void> result) {
    try {
      if (err.address != 0 && err.ref.code != 0) {
        final code = err.ref.code;
        final msg = err.ref.message.toDartString();
        _weaveffiErrorClear(err);
        completer.completeError(WeaveFFIException(code, msg));
        return;
      }
      completer.complete(TaskResult._(result));
    } catch (e) {
      completer.completeError(e);
    } finally {
      callable.close();
    }
  });
  try {
    _weaveffiTasksRunTaskAsync(namePtr, callable.nativeFunction, nullptr);
  } catch (e) {
    callable.close();
    calloc.free(namePtr);
    rethrow;
  }
  return completer.future.whenComplete(() {
    calloc.free(namePtr);
  });
}

The callable is closed in the callback’s finally (or immediately if the launch itself throws), so each native trampoline is freed exactly once; input buffers are released in whenComplete once the future settles. The dart:async import is only emitted when the IDL contains at least one async function.

For functions marked cancellable: true the C launcher gains a weaveffi_cancel_token* parameter. The Dart wrapper passes nullptr for it and doesn’t expose the token; only the C, C++, and Kotlin targets surface cancellation tokens.

Iterators

iter<T> returns surface as Iterable<T>. The wrapper launches the iterator, drains it eagerly into a List<T> through the generated _next binding, then destroys the iterator handle:

/// Return an iterator over all sent messages
Iterable<String> getMessages() {
  final err = calloc<_WeaveFFIError>();
  try {
    final iter = _weaveffiEventsGetMessages(err);
    _checkError(err);
    final items = <String>[];
    final outItem = calloc<Pointer<Utf8>>();
    while (_weaveffiEventsGetMessagesIteratorNext(iter, outItem, err) != 0) {
      _checkError(err);
      items.add(outItem.value.toDartString());
    }
    _checkError(err);
    calloc.free(outItem);
    _weaveffiEventsGetMessagesIteratorDestroy(iter);
    return items;
  } finally {
    calloc.free(err);
  }
}

Troubleshooting

  • Invalid argument(s): Failed to load dynamic library: the cdylib is not on the search path. Set DYLD_LIBRARY_PATH / LD_LIBRARY_PATH or copy the library next to your executable.
  • UnsupportedError: Unsupported platform: the loader maps to darwin, linux, and windows. Other platforms (Android, iOS) use the Flutter integration where the framework opens the library.
  • MissingPluginException in Flutter: that error is unrelated to WeaveFFI; double-check that you depend on the generated package and haven’t shadowed it with a different weaveffi dependency.
  • Strings appear truncated: Rust strings aren’t nul-terminated; make sure toDartString() is reading the pointer returned from a generated getter, not a raw pointer.