#dart #flutter #zero-copy #bindings #codegen #ffi #memory-management

membrane

Membrane is an opinionated crate that generates a Dart package from a Rust library. Extremely fast performance with strict typing and zero copy returns over the FFI boundary via bincode.

20 releases (8 breaking)

0.11.0 Sep 15, 2023
0.9.6 Jul 21, 2023
0.6.3 May 20, 2022
0.6.2 Mar 25, 2022
0.3.3 Nov 17, 2021

#182 in Development tools

Download history 12812/week @ 2024-08-08 12757/week @ 2024-08-15 13631/week @ 2024-08-22 11974/week @ 2024-08-29 15056/week @ 2024-09-05 14933/week @ 2024-09-12 16749/week @ 2024-09-19 16284/week @ 2024-09-26 16945/week @ 2024-10-03 14023/week @ 2024-10-10 18657/week @ 2024-10-17 15925/week @ 2024-10-24 19474/week @ 2024-10-31 22660/week @ 2024-11-07 22757/week @ 2024-11-14 20093/week @ 2024-11-21

87,575 downloads per month

Apache-2.0

115KB
3K SLoC

Membrane

Membrane is an opinionated crate that generates a Dart package from your Rust library. It provides extremely fast performance with strict typing, automatic memory management, and zero copy returns over the FFI boundary via bincode.

Membrane diagram

Development Environment

On Linux ffigen looks for libclang at /usr/lib/llvm-11/lib/libclang.so so you may need to symlink to the version specific library: ln -s /usr/lib/llvm-11/lib/libclang.so.1 /usr/lib/llvm-11/lib/libclang.so.

Usage

View the example directory for a runnable example.

In your crate's lib.rs add a RUNTIME static that will survive for the lifetime of the program. RUNTIME must hold an instance of membrane::App<Runtime> where Runtime has the membrane::Interface trait implemented for whichever async framework you wish to use. In our examples we use tokio to provide the runtime behavior, you are welcome to copy it:

use membrane::runtime::{App, Interface, AbortHandle};

pub struct Runtime(tokio::runtime::Runtime);

impl Interface for Runtime {
  fn spawn<F>(&self, future: F) -> AbortHandle
  where
    F: std::future::Future + Send + 'static,
    F::Output: Send + 'static,
  {
    let handle = self.0.spawn(future);
    AbortHandle {
      abort: Box::new(move || handle.abort()),
    }
  }

  fn spawn_blocking<F, R>(&self, future: F) -> AbortHandle
  where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
  {
    let handle = self.0.spawn_blocking(future);
    AbortHandle {
      abort: Box::new(move || handle.abort()),
    }
  }
}

static RUNTIME: App<Runtime> = App::new(|| {
  Runtime(
    tokio::runtime::Builder::new_multi_thread()
    .worker_threads(2)
    .thread_name("libexample")
    .build()
    .unwrap()
  )
});

Then write some code that is annotated with the #[async_dart] macro. No need to use C types here, just use Rust String, i64, f64, bool, structs, or enums as usual (or with Option). The functions can be anywhere in your program and may return either an async Result<T, E> or an impl Stream<Item = Result<T, E>>:

use membrane::async_dart;
use tokio_stream::Stream;

use crate::data;

#[async_dart(namespace = "accounts")]
pub fn contacts() -> impl Stream<Item = Result<data::Contact, data::Error>> {
  futures::stream::iter(vec![Ok(Default::default())])
}

#[async_dart(namespace = "accounts")]
pub async fn contact(id: String) -> Result<data::Contact, data::Error> {
  Ok(data::Contact {
    id: id.parse().unwrap(),
    ..Default::default()
  })
}

And now you are ready to generate the Dart package. Note that this code goes in a bin/generator.rs or similar to be ran with cargo run or a build task rather than in build.rs (which only runs before compilation):

fn main() {
  // if nothing else in this generator.rs references lib.rs then
  // at least call a dummy function so lib.rs doesn't get optimized away
  example::load();

  let mut project = membrane::Membrane::new();
  project
    // name the output pub directory
    .package_destination_dir("../dart_example")
    // the pub package name, if different than the directory
    .package_name("example")
    // give the basename of the .so or .dylib that your Rust program provides
    .using_lib("libexample")
    // use Dart enums instead of class enums
    .with_c_style_enums(true)
    .create_pub_package()
    .write_api()
    .write_c_headers()
    .write_bindings();
}

If everything went as planned you can now call Rust from Dart with:

cd example
cargo run
cargo build
cd ../dart_example
cp ../example/target/debug/libexample.dylib .
dart --enable-asserts run

(--enable-asserts enables a pretty print toString() in the generated classes)

import 'package:dart_example/accounts.dart';

void main(List<String> arguments) async {
  var accounts = AccountsApi();
  print(await accounts.contact(id: "1"));
}

If you get an error on Linux about not being able to load libexample.so then add the pub package's path to LD_LIBRARY_PATH.

Dependencies

~7–14MB
~175K SLoC