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!