Calling C functions from Deno — Part 1 Passing primitive types

Mayank C
Tech Tonic

--

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 C shared library into Deno, and calling a couple of C functions provided by that library from Deno. We’ll make system calls in the C 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<S extends Record<string, ForeignFunction>>(
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 C functions

Step 1: Write a C program

The first step is to write a C program. Our C program has two functions, each makes a system call.

This program is specifically for mac

//denoFfiTest.cc#include <sys/types.h>
#include <sys/sysctl.h>
extern "C" int numLogicalCpus() {
int ret = 0;
size_t size = sizeof(ret);
if (sysctlbyname("hw.logicalcpu", &ret, &size, NULL, 0) == -1)
return -1;
return ret;
}
extern "C" int numPhysicalCpus() {
int ret = 0;
size_t size = sizeof(ret);
if (sysctlbyname("hw.physicalcpu", &ret, &size, NULL, 0) == -1)
return -1;
return ret;
}

Step 2: Compile the program

The second step is to compile the program. We just want to compile and create an object file. No linking is required.

> g++ -c denoFfiTest.cc
> ls -l
-rw-r--r-- 1 mayankc staff 1088 Aug 13 10:48 denoFfiTest.o

Step 3: Build shared library

The third step is to take all the object files (in this example there is only one), and build a shared library.

> g++ -dynamiclib -fPIC -o systeminfo.dylib denoFfiTest.o
> ls -ltr
-rw-r--r-- 1 mayankc staff 1088 Aug 13 10:48 denoFfiTest.o
-rwxr-xr-x 1 mayankc staff 12580 Aug 13 10:48 systeminfo.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 = `systeminfo.dylib`;
const dylib = Deno.dlopen(libName, {
"numLogicalCpus": { parameters: [], result: "i32" },
"numPhysicalCpus": { parameters: [], result: "i32" }
});

If the dlOpen function is able to open the shared library & import the symbols, it’d return a DynamicLibrary object. Here is the dump of that object:

dylib; //DynamicLibrary { symbols: { numLogicalCpus: [Function], numPhysicalCpus: [Function] } }

As we can see, there are two symbols: numLogicalCpus & numPhysicalCpus.

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.numLogicalCpus();  //8
dylib.symbols.numPhysicalCpus(); //4

The functions do work! On my MacBook, I get 8 logical CPUs and 4 physical CPUs.

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 rust 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)

--

--