Call Rust from TypeScript with Bun


The Announcement

Hi there! đź‘‹ A few days ago I stumbled across a very exciting announcement in the Bun v1.0 presentation.

In the section about how eslint plugins integrate natively into bun, there was a small example showcasing a rust file directly being imported into a TypeScript file.

import { add } from "./math.rs";

A “bun enthusiast” (who after a little digging through the bun discord server turned out to be tr1ckydev) used Buns foreign function interface (FFI1) functionalities to achieve this. After more searching I found out that tr1ckydev is working on a package called hyperimport which at the time of writing is still an empty repository.

I was very excited about this and wanted to try it out myself. So I created a small example project to see how it works.


Warning: This is a very early stage of the bun ecosystem and the FFI is still in development. I’m strongly discouraging using an implementation in this barebone state anywhere. There are still a lot of caveats and rough edges, at the end this is just a showcase implementation.


If you’re interested in digging deeper, I’ll append the links to the specifications I’m referring to at the end of the page.

The Experiment

Turns out, it’s not even that hard to get a working example. I’m not talking about a full-blown production-ready setup, but getting a small working example is pretty straightforward.

I’m also linking the repository here, so you can follow along if you want to.

Before we get startet we need Bun and rust installed

The central code is in the src/main.ts file. It’s also the file with the most, let’s say, “magic” going on.

As in the example simply adding two numbers (or isize in rust terms) is not fast enough, so instead, we do it blazingly fast in rust.

// main.ts
import { add } from "./add.rs";

console.log(add(1, 2));

When was the last time a simplified “Hello world” example was this exciting?

The add.rs file contains only a plain function that adds two numbers and returns the result . We expose it via the C Application Binary Interface (ABI) to make it callable from the Bun FFI1 implementation.

// add.rs
#[no_mangle]
pub extern "C" fn add(a: isize, b: isize) -> isize {
    a + b
}

With this set up, we’ll now implement a loader for the rust file using the Bun loader plugin API2. There are already a few loaders available, next to the standard ones like js, ts or tsx also wasm imports just to name a few. To also support the rs extension, we’ll need to add a custom loader first.

To do this, bun exposes a type alias called BunPlugin which we can use to create our loader. The setup function receives an object holding an onLoad-Method which filters and transforms the files we want to load. After creating it, we can register it using the plugin function.

// rust.ts
import { plugin, type BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "RustPlugin",
  setup(build) {
    build.onLoad(
      {
        filter: /\.rs$/, // filter for imported .rs files
      },
      (args) => {
        // transforms
        return {
          exports: {}, // here the ABI will be injected
          loader: "object",
        };
      },
    );
  },
};

plugin(myPlugin);

With this setup, we can now focus on the loaders implementation. The loader has two responsibilities:

  1. Compile the rust file to a shared library
  2. Link the shared library to the bun runtime

To compile the rust file into a shared library the bun docs recommend using the cdylib target3:

rustc --crate-type cdylib add.rs

To translate this into Buns typescript syntax, we invoke Bun.spawnSync supplying the rust-files path as the last argument.

// rust.ts
// ...
Bun.spawnSync(["rustc", "--crate-type=cdylib", args.path], {
  env: process.env,
});
// ...

With this not so safe implementation the file should get compiled into a libadd.dylib file (on mac). Now, we need to link this file to the bun runtime. Bun also has a trick for that up its sleeve.

We’ll use the dlopen function from the bun:ffi module to load the shared library into the runtime4.

// rust.ts
import { dlopen, FFIType, suffix } from "bun:ffi";
// ...
const path = `lib${filename}.${suffix}`;
const lib = dlopen(path, {
  add: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
});

return {
  exports: lib.symbols,
  loader: "object",
};
// ...

The last step is telling bun to load the plugin before executing any code. Bun uses a bunfig.toml file to configure the runtime. We’ll add the plugin file to the preload array5.

# bunfig.toml
preload = ["./rust.ts"]

Now, if we run bun run main.ts we should see the result of the add function in the console.

And here is the whole implementation for the loader plugin:

// rust.ts
import { plugin, type BunPlugin } from "bun";
import { dlopen, FFIType, suffix } from "bun:ffi";

const myPlugin: BunPlugin = {
  name: "RustPlugin",
  setup(build) {
    build.onLoad(
      {
        filter: /\.rs$/,
      },
      (args) => {
        Bun.spawnSync(["rustc", "--crate-type=cdylib", args.path], {
          env: process.env,
        });

        // get rust file name
        const filename = args.path.split("/").pop()!.split(".")[0];

        const path = `lib${filename}.${suffix}`;
        const lib = dlopen(path, {
          add: {
            args: [FFIType.i32, FFIType.i32],
            returns: FFIType.i32,
          },
        });

        return {
          exports: lib.symbols,
          loader: "object",
        };
      },
    );
  },
};

plugin(myPlugin);

Conclusion

Unfortunately the dlopen function needs to know the shape of the function you want to import. So for now, this solution will only work for rust files with add functions. There might be a way to get around this, but I haven’t found it yet.

I hope this was an interesting read and you learned something new. I’m very excited about the bun ecosystem and can’t wait to see what the future holds. Hopefully this will be a good starting point for you to experiment with bun and the FFI.


References

Footnotes

  1. https://bun.sh/docs/api/ffi ↩ ↩2

  2. https://bun.sh/docs/runtime/plugins#loaders ↩

  3. https://bun.sh/docs/api/ffi#rust ↩

  4. https://bun.sh/docs/api/ffi#zig ↩

  5. https://bun.sh/docs/runtime/bunfig#preload ↩