« All posts

01.30.2022 - Deno / Introduction to FFI API

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!

Read more