01.30.2022 - Deno/Introduction to FFI API

All posts

Posted On 01.30.2022

From version 1.13, Deno introduced the FFI API (foreign function interface API) that allows us to load libraries built in any language that supports the C ABI (like C/C++, Rust, Zig, Kotlin,…).

To load the library, we use the Deno.dlopen method and supply the call signature of the functions we want to import.

For example, create a Rust library and export a method called hello():

#[no_mangle]
pub fn hello() {
    println!("Hello! This is Rust!");
}

Specify the crate type in Cargo.toml and build it with cargo build:

[lib]
crate-type = ["cdylib"]

In debug mode, the output library will be located at target/debug/librust_deno.dylib, where rust-deno is the project name.

Load this library in Deno with the following code:

const libName = './target/debug/librust_deno.dylib';
 
const dylib = Deno.dlopen(libName, {
    "hello": { parameters: [], result: "void" }
});
 
const result = dylib.symbols.hello();

Finally, run it with Deno, we have to specify the --allow-ffi and --unstable flags:

deno run --allow-ffi --unstable demo.ts

We will see the output on the screen:

Hello! This is Rust!

Notice how we have to specify the call signature of the hello() function when importing it from Rust:

"hello": { parameters: [], result: "void" }

In reality, you might not want to export and import things manually between Rust and Deno, when it comes to more complex data types like string, things get messy.

Let’s take a look at our new hello method, we want to pass a string as a pointer from Deno to Rust, this means, we have to pass along the length of the string too:

use core::slice;
use std::ffi::CStr;
 
#[no_mangle]
pub fn hello(ptr: *const u8, len: usize) {
    let slice = unsafe { slice::from_raw_parts(ptr, len) };
    let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(slice) };
    println!("Hello, {}!", cstr.to_str().unwrap());
}

Now, the Deno code:

const libName = './target/debug/librust_deno.dylib';
 
const dylib = Deno.dlopen(libName, {
    "hello": { parameters: [ "pointer", "usize" ], result: "void" }
});
 
const nameStr = "The Notorious Snacky";
const namePtr = new Uint8Array([
    ...new TextEncoder().encode(nameStr),
]);
const result = dylib.symbols.hello(namePtr, nameStr.length + 1);

Let’s see what’s going on here. On the Rust side, we take a *const u8 pointer and a length of the string, then convert that pointer into a Rust string with some unsafe codes. From the Deno side, we have to encode the string to a byte array and pass the pointer of that array to the Rust code.

The deno_bindgen project offered a convenient way to work with data types across the language boundaries, just like wasm-bindgen in Rust WASM.

First, import the deno_bindgen crate in your Rust code:

[dependencies]
deno_bindgen = "0.4.1"

Don’t forget to install the deno_bindgen-cli tool, because we are going to use this tool to build instead of cargo:

deno install -Afq -n deno_bindgen https://deno.land/x/deno_bindgen/cli.ts

Now, in your Rust code, just export or use things as normal:

use deno_bindgen::deno_bindgen;
 
#[deno_bindgen]
pub fn hello(name: &str) {
    println!("Hello, {}!", name);
}

Run deno_bindgen to build your code, the output will be a bindings/bindings.ts file in your project’s root:

deno_bindgen

And in your Deno code, simply import the function and call it:

import { hello } from './bindings/bindings.ts';
 
hello("The Notorious Snacky");

Finally, run the code:

deno run -A --unstable demo.
 
# Hello, The Notorious Snacky!

The new FFI API opened up a lot of possibilities for Deno/TypeScript, for example, there are projects like deno_sdl2 that allows us to create native SDL2 applications using Deno and TypeScript, no more Electron!