dodo-bindgen-cli 2.0.0

A CLI tool for generating TypeScript bindings for libraries built with the `dodo_bindgen` crate
dodo-bindgen-cli-2.0.0 is not a library.

dodo-bindgen

dodo-bindgen is an FFI bindings generator that simplifies writing Rust libraries for Dodo.

It works by providing procedural macros that create wrappers around native Rust functions and make it FFI-safe. This makes writing FFI libraries easier as it deals with boilerplate code generation so you don't have to.

It also provides a CLI tool that generates a TypeScript module and tries to follow the semantics of your code wherever possible.

This project aims to empower TypeScript development by bridging access to Rust's powerful memory-safe ecosystem.

To use

Add the dodo-bindgen crate to your library crate's dependencies (note the spelling and dash - separator)

# Cargo.toml
[dependencies]
dodo-bindgen = "2.0"

Use the dodo_bindgen attribute macro on a function, implement, or struct item in your source code:

// lib.rs
use dodo_bindgen::dodo_bindgen;

#[dodo_bindgen]
fn test_1() {
    println!("Hello, world!");
}

#[dodo_bindgen]
fn test_2(string: String) -> String {
    format!("{} to Rust!", string)
}

Next, to generate the bindings and TypeScript code, you must install the CLI tool with the command below:

cargo install dodo-bindgen-cli

Then, run this command in your project's package folder (not the workspace folder).

dodo-bindgen

This will automatically generate a TypeScript module in <pkg_root>/lib/<your_module>, along with another module rust_type.ts that contains TypeScript representations of Rust types. Generated files are formatted with deno fmt, so your deno.json formatting options apply.

Finally, you can write your TypeScript code and import the functions from the Rust code. Function and method names are converted to camelCase (test_1 becomes test1), and a static fn new() -> Self becomes the class constructor.

// hello_world.ts
import { RustString, test1, test2 } from "./lib/libmy_mod.ts";

test1();

Deno.test("test_string", () => {
    // create a `RustString` instance from a JavaScript string
    // by calling the `from()` static method
    const hello_string = test2(RustString.from("Hello from Deno"));

    // turn the RustString into a JavaScript string
    // by calling its `into()` method
    console.log(hello_string.into());
})

To run:

deno run --allow-all --unstable-ffi hello_world.ts

This should output:

Hello, world!
Hello from Deno to Rust!

For additional code generation options, run dodo-bindgen --help

Primitive types

Numeric types map to number (64-bit and pointer-sized integers to bigint), bool to boolean and char to a single-character string.

JavaScript strings

String values cross the boundary as RustString handles. To work with plain JavaScript strings instead, use JsStr (borrowed, parameters only) and JsString (owned, parameters and return values):

#[dodo_bindgen]
fn greet(name: JsStr) -> JsString {
    format!("Hello, {name}!").into()
}
greet("Deno"); // "Hello, Deno!"

Typed arrays

JavaScript typed arrays map to JsSlice<T> (borrowed, zero-copy), JsSliceMut<T> (borrowed mutably, zero-copy, writes are visible to JavaScript) and JsVec<T> (owned; copied from parameters, returned as a fresh typed array). T is one of u8, i8, u16, i16, u32, i32, u64, i64, f32 or f64:

#[dodo_bindgen]
fn scale(mut values: JsSliceMut<f32>, factor: f32) {
    for value in values.iter_mut() {
        *value *= factor;
    }
}

#[dodo_bindgen]
fn range(len: u32) -> JsVec<u32> {
    (0..len).collect()
}
const values = new Float32Array([1, 2, 3]);
scale(values, 2); // values is now [2, 4, 6]
range(3); // Uint32Array [0, 1, 2]

Buffers passed to non_blocking functions are kept alive until the returned promise settles.

Extending foreign types

Types from other crates can be exposed as TypeScript classes with a foreign impl block. The block is turned into a private extension trait, so the orphan rule does not apply. Concrete instances of generic types work too:

#[dodo_bindgen(foreign, name = "Bytes")]
impl Vec<u8> {
    fn new() -> Self {
        Vec::new()
    }

    fn len(&self) -> usize {
        self.len()
    }
}

Every signature using Vec<u8> is then typed as Bytes in TypeScript. RustString itself is defined this way inside dodo-bindgen. Foreign classes that no exported item refers to are left out of the generated module.

Custom TypeScript

Additional TypeScript class members can be attached to any exported struct, enum or impl block:

#[dodo_bindgen(ts = "override toString(): string { return this.into(); }")]
#[dodo_bindgen(ts = include_str!("my_class.ts"))]

Every generated class also implements [Symbol.dispose], so instances can be released with using.

Examples

Check out the test directory to see some examples