The JS runtimes
Published in

The JS runtimes

Calling Rust functions from Deno — Part 1 Passing primitive types

Introduction

From Deno v1.13, Deno has completely revamped the mechanism of loading & calling functions provided by a shared library from Deno runtime. It used to be called plugin (openPlugin). Now it has a more standardized name: FFI (Foreign Function Interface).

The definition of FFI is (source Wikipedia):

A foreign function interface (FFI) is a mechanism by which a program written in one programming language can call routines or make use of services written in another. The primary function of a foreign function interface is to mate the semantics and calling conventions of one programming language (the host language, or the language which defines the FFI), with the semantics and conventions of another (the guest language).

In simple words, FFI allows calling functions provided by a different language.

In this article, we’ll learn about the FFI feature by loading a Rust shared library into Deno, and calling a couple of Rust functions provided by that library from Deno. We’ll make system calls in the Rust library.

Basics

Imports

The primary function for loading a shared library is dlOpen which is part of the Deno core runtime. Therefore, no imports are required.

Permissions

To use this feature, two command-line flags are required:

  • — allow-ffi
  • — unstable

Types

The FFI supports only types like void, signed/unsigned integer, floating point numbers, buffers, pointers, etc. In this article, we’ll cover primitive types like integers. A subsequent article will cover more complex types like buffers, etc.

Here is the complete list of types supported by FFI:

Native types =>    Void,
U8,
I8,
U16,
I16,
U32,
I32,
U64,
I64,
USize,
ISize,
F32,
F64,
Pointer

DlOpen

The function to open a dynamic library is: dlOpen. There are two mandatory parameters:

  • filename: Path of the shared library to open
  • symbols: The symbols to import from the shared library along with the signature (input & output). For input, an array of parameters needs to be specified (it could be empty). For output, a single result needs to be specified (it could be void).
function dlopen (
filename: string,
symbols: S,
): DynamicLibrary<S>;

For example-

const dylib = Deno.dlopen('/var/lib/xyz.dylib', {
"print_something": { parameters: [], result: "void" },
"add": { parameters: ["u32", "u32"], result: "u32" },
});

In the above example, print_something & add symbols/functions are imported from the library.

Calling symbols

Once the shared library is opened & symbols are imported, they can be called anytime by optionally passing the required parameters (as per the signature).

For example-

dylib.symbols.print_something();
dylib.symbols.add(123, 456);

Closing the library

When the library’s use is completed, it can be closed by calling the close function.

dylib.close();

Now, let’s go over the steps in detail.

Steps to call Rust functions

Step 1: Write a Rust program

The first step is to write a Rust program. Our Rust program has two functions, each makes a system call. To make system calls, it uses an external crate called sysctl.

//lib.rsextern crate sysctl;
use sysctl::Sysctl;
#[no_mangle]
pub extern "C" fn get_hw_ncpu() -> i32 {
let v=sysctl::Ctl::new("hw.ncpu").unwrap().value().unwrap().to_string();
let r=v.parse::<i32>().unwrap();
return r;
}
#[no_mangle]
pub extern "C" fn get_hw_active_cpu() -> i32{
let v=sysctl::Ctl::new("hw.activecpu").unwrap().value().unwrap().to_string();
let r=v.parse::<i32>().unwrap();
return r;
}

Step 2: Write a build file

The second step is to write a build file that can create a shared library. The build file produces a cdylib and uses sysctl as its dependency.

//Cargo.toml[package]
name = "deno_ffi_test"
version = "0.1.0"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
sysctl = "0.4.2"

Step 3: Build a shared library

The third step is to compile the source and build a shared library. This is done using a single command: cargo build.

denoFfiTest: cargo build
Compiling proc-macro2 v1.0.28
Compiling unicode-xid v0.2.2
Compiling syn v1.0.75
Compiling libc v0.2.100
Compiling byteorder v1.4.3
Compiling bitflags v1.3.2
Compiling quote v1.0.9
Compiling thiserror-impl v1.0.26
Compiling thiserror v1.0.26
Compiling sysctl v0.4.2
Compiling deno_ffi_test v0.1.0 (/Users/mayankc/Work/source/denoExamples/denoFfiTest)
Finished dev [unoptimized + debuginfo] target(s) in 8.86s
denoFfiTest: cargo clean
denoFfiTest: cargo build
Compiling proc-macro2 v1.0.28
Compiling unicode-xid v0.2.2
Compiling syn v1.0.75
Compiling libc v0.2.100
Compiling bitflags v1.3.2
Compiling byteorder v1.4.3
Compiling quote v1.0.9
Compiling thiserror-impl v1.0.26
Compiling thiserror v1.0.26
Compiling sysctl v0.4.2
Compiling deno_ffi_test v0.1.0 (/Users/mayankc/Work/source/denoExamples/denoFfiTest)
Finished dev [unoptimized + debuginfo] target(s) in 7.88s

The output is present in the target folder:

denoFfiTest: ls target/debug/
build examples libdeno_ffi_test.d
deps incremental libdeno_ffi_test.dylib

Step 4: Open shared library in Deno

The fourth step is to open our shared library in Deno. This step uses dlOpen function. Both the symbols/functions are imported from the shared library. None of the functions take input. Both of the functions produce a 32-bit signed integer as output.

const libName = `denoFfiTest/target/debug/libdeno_ffi_test.dylib`;
const dylib = Deno.dlopen(libName, {
"get_hw_ncpu": { parameters: [], result: "i32" },
"get_hw_active_cpu": { parameters: [], result: "i32" }
});

Step 5: Calling the functions

The fifth step is when we call the functions provided by the shared library. This is why we did all the work!

dylib.symbols.get_hw_ncpu(); //8
dylib.symbols.get_hw_active_cpu(); //8

Step 6: Closing the library

The last step is to close the library as we’re done with it.

dylib.close();

To know about how to call C functions from Deno with arbitrary buffers, the article can be seen here.

To know about how to call C functions from Deno with primitive types, the article can be seen here.

To know about how to call rust functions from Deno with arbitrary buffers, the article can be seen here.

To know about how to call C functions from Deno with primitive types, the article can be seen here. (Windows)

To know about how to call C functions from Deno with arbitrary buffers, the article can be seen here. (Windows)

This story is a part of the exclusive medium publication on Deno: Deno World.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store