Calling C functions from Windows DLL in Deno — Part 2 Buffers

Mayank C
Tech Tonic

--

Introduction

Deno supports loading & calling of native functions provided by a shared library (mostly written in C, C++, or Rust). The standardized name for this mechanism is: 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.

The first article in the Windows series covered passing primitive types like integers, floats, etc.

In the second article (i.e. this article), we’ll learn how to pass buffers containing string data to and from the native library.

Let’s get started.

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('C:\\shared\\libs\\some_library.dll', {
"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 a single function, called to_and_fro_buffer, that takes a void* as input and returns a char*. The void* is typecasted to char* in the C code. In other words, the function takes an arbitrary string buffer as the input, and returns another string buffer.

The Visual Studio IDE is very useful in building a DLL for the C program.

Visual Studio community edition is free for personal use. It can be downloaded from here.

To create a DLL project, choose File -> new -> project from Visual studio:

Then, using project -> add item, create a header file and a source file:

We’ve created a header file called BufferType.h and a source file called BufferType.cpp. The function to_and_fro_buffer takes a void* as input, adds them and returns a char* (string).

Step 2: Build DLL

The second step is to build a DLL from the project.

To build DLL, go to Build -> Build solution:

This builds the DLL and places it in a predefined path under the project’s root directory:

The DLL is now ready for use.

Step 3: Open DLL in Deno

The third step is to open our DLL in Deno. This step uses dlOpen function. The to_and_fro_buffer symbol is imported from the DLL. As the function a void* input and returns a char*, the same needs to specified when the function is imported from the DLL in dlOpen.

Step 4: Calling the functions

The fourth step is when we call the function provided by the DLL. This is exactly why we did all the work!

The to_and_fro_buffer function needs a buffer as the input and returns a buffer as the output.

  • The input buffer must be provided as a Uint8Array (like bytes)
  • The output buffer needs to be ‘viewed’ using UnsafePointerView and then the string can be obtained by calling getCString function

Here is the complete code:

Here is the output of a run:

The native function gets called successfully with an arbitrary buffer as the input. The same native function is able to return an arbitrary buffer data to the Deno user space. The first line of the output comes from the C function and the second line of the output comes from Deno space.

That’s all about calling C functions from Deno with to and fro of arbitrary buffers.

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 primitive types, the article can be seen here. (Mac)

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

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

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

--

--