I recently encountered a problem in my project where I needed to call C++ code in NodeJS, so here’s a quick summary.

The main options

In NodeJS, there are two main options for communicating with code written in other languages.

  • Using the AddOn technique, write an extension to NodeJS using C++ and then call the source code or dynamic libraries written in other languages in the code
  • using the FFI (Foreign Function Interface) technique, which brings in dynamic libraries written in other languages directly in Node

A comparison of these two approaches shows that each has its own advantages and disadvantages.

First, the AddOn technique is more general, it can use C++ code to extend the behavior of Node, and many libraries use this approach to do some of the more low-level operations (such as communication with the operating system). However, it is a bit tricky to write a C++ project, export the corresponding functions according to the NodeJS specification, and compile it every time you install it (to fit the local Node version). If you just call a DLL, you also need to repackage the DLL’s interface in your project.

If you use FFI technology, it is more limited. First, it can only call other dynamic libraries, and if you want to do more with C/C++, you need to wrap another layer of DLL, and it only supports the _cdecl calling convention (that is, the DLL must be marked with the _cdecl compile command when exported), not _stdcall or _fastcall calls are not supported. However, it is easy to call the DLL by declaring the interface directly in the JS code.

In comparison, if you only call third-party DLLs (and they happen to be _cdecl exports), you can’t go wrong with FFI (although there may be some performance loss and debugging difficulties).

In fact, in theory, FFI is also based on AddOn technology, except that it can help you convert interfaces defined in JS directly into C interfaces and share them with the loaded DLLs using NodeJS’s Buffer memory. Of course, this generality of FFI also results in some performance loss.

Let’s talk about using FFI on Windows as an example of how to use NodeJS to communicate with a DLL compiled from C++.

FFI preparation for use

Installing NodeJS

You may already have NodeJS in your environment, but if it’s the latest version, there will be various compatibility issues when installing FFI (for example, the compilation won’t pass, and although someone has provided a patch, it hasn’t been merged into the master branch yet, so to avoid bugs, it’s better not to use it yet). So you can install the LTS version instead.

Also, you need to pay attention to whether the DLL to be called is 32-bit or 64-bit, and the version of the Node needs to match the version of the DLL. If a 64-bit Node calls a 32-bit DLL, it will not be loaded successfully, and vice versa.

Installing the C++ toolchain for Windows

There are two options here.

  • Install Visual Studio and install the corresponding toolchain. If you are using VS 2019, you need to install C++ desktop development and Windows SDK related tools (Node v10 now only supports MSVC for v141), this way it is easier to debug later (although it is also tough)
  • After installing Node, run Powershell with administrator privileges and install windows-build-tools globally, refer to the command npm install --global --production windows-build-tools

Install node-gyp

node-gyp is a gyp-based cross-platform build tool in Node for building other libraries.

When installing it, you need to use the VC toolchain, so if you don’t have the toolchain in a global variable, you need to open VS’s Developer Powershell installation, which is usually found in the Visual Studio folder in the start menu.

Reference command: npm install -g node-gyp

Installing FFI and REF

The following steps still require the VC toolchain, so they may still need to be executed in Developer Powershell (it is recommended to always have this window available for any commands that involve compiling and installing later).

If you install the FFI and related tools without the VC toolchain, you will install the binary code directly, which may result in the ABI version of the package not matching the ABI version of NodeJS (mentioned in the Tips below).

Now, switch to the project’s folder and install the following packages. The ffi package supports FFI functionality, the ref package supports pointer functionality (the principle is to convert JS structures to C structures via Node’s Buffer memory), and the ref-* package supports advanced structures (such as arrays and structs).

1
2
3
4
npm install ffi -s
npm install ref -s
npm install ref-array -s
npm install ref-struct -s

In addition, you can also install the ref-wchar package if you want to support the common wchar type in VC.

Installing the electron-rebuild package

For electron projects, it is also recommended to install the electron-rebuild package, which iterates through all packages in the node_modules directory and recompiles them.

Then, the recommended command to configure electron-rebuild in package.json is.

1
2
3
"scripts": {
  "rebuild": "./node_modules/.bin/electron-rebuild"
}

After that, you can just run npm run rebuild when you need to recompile.

How to use

Simple overview

The following official example can be viewed.

1
2
3
4
5
6
var ffi = require('ffi')

var libm = ffi.Library('libm', {
  ceil: ['double', ['double']]
})
libm.ceil(1.5) // 2

After the introduction of FFI, the libm library is called using FFI (perhaps this example can only be used on Unix-like systems) with the general extension libm.so. The system searches for this dynamic library in the system directory and loads it into the node process using the dynamic linker.

Next, the program declares a method ceil (round up) in the libm library, where the return value of the function is of type double (doule in the first array), and the input to the function is also a value of type double (double in the second array).

Finally, the function in the dynamic library can be called directly using the libm.ceil method and return the correct value.

This is just a simple use case for FFI, more complex usage (mainly asynchronous calls and callback functions) can be found on the FFI examples page at https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial.

Types

The type system of FFI actually remembers the types of the ref library. The type system of the ref library is based on the Buffer memory of NodeJS, which allows you to access and modify the data in Buffer memory based on the type of data in Buffe.

The data types that come with ref are all basic types, such as int, bool, or string. All types can be found in the ref wiki.

Most types in ref are abbreviated, for example ref.types.int can be abbreviated to int.

Note that char* can be written as string, and the corresponding ref type is ref.types.CString. It is important to note that string is a basic type in JS, but a reference type in C.

For pointer types, ref provides a method ref.refType() to get them, e.g. int* type can be obtained using ref.refType('int'). Of course, to save time, you can also use int* directly.

For pointer dereferencing, the ref library also provides a deref() method. Just use this method on a variable of the corresponding type to get the variable whose contents the pointer points to. For example, if a JS variable a_pointer of type int* is pointed to, then we can use the a_pointer.deref() method if we want to get the specific integer value.

Conversely, if you want to get the address of a variable, you need to use the ref() method on a variable.

You need to be careful to master the difference between types and variable values, using ref.types, ref.refType or ref_struct({...}) as will be mentioned below `, and if you want to get a variable of a certain type, there are two ways to get it, one is to get it from the return value of the FFI function, and the other is to open a space in the Buffer to hold the variable of the type obtained, as will be described below.

If you need to open up a space in the NodJS Buffer of a certain type, you can use the ref.alloc() function and just pass in the type name. For example, if you want to open up a memory of type int, you can use ref.alloc('int') to get it.

In addition, the following points need to be noted.

  • If opening memory of type string, it is recommended to use the method ref.allocCString, whose argument is a JS string. Because C strings have a \0 identifier at the end, it is safer to use this method to get C strings.
  • If the value is NULL in C, the corresponding value in JS is ref.NULL.
  • If you encounter a pointer type, you can uniformly use 'void' or ref.types.void to represent it.
  • If you want to represent a pointer to a function, you can use 'pointer' to represent it.

For composite types, such as arrays or structures, the ref library itself does not provide support for them, and you need to use the ref-array and ref-struct libraries to implement them.

In addition, there is also a ref-based library ref-wchar that supports the more common wide character wchar type in the Windows API.

Finally, the documentation for ref is attached at http://tootallnate.github.io/ref/, where the specific APIs can be consulted.

Calling external symbols

Suppose we have the following C code (and make it more complex).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* main.c */
typedef struct t_s_t{
    int a;
    char b;
} t_s;

__declspec(dllexport) int add_one(int a) {
    return a + 1;
}

__declspec(dllexport) void struct_test(t_s** t_s_p) {
    *t_s_p = (t_s *)malloc(sizeof(t_s));
    (*t_s_p)->a = 1;
    (*t_s_p)->b = 'd';
}

The above code declares a structure t_s and two functions add_one and struct_test. The __declspec tag in front of the function indicates that it is declared as an exported function. VC exports C functions by default using the _cdecl calling convention.

The add_one method does the obvious thing, it returns the incoming arguments plus one more. The struct_test function, on the other hand, creates a memory space on the heap of the size of the raw declaration structure, then assigns a pointer to that memory space to the incoming argument and assigns a value to the structure. (The code here is actually not rigorous enough to perform memory reclamation, but that is not the focus of this article, so let’s not discuss it first)

Note that in the case of C++ code, you need to use the extern "C" tag to export it, otherwise you won’t be able to find the function through the symbols in the source code due to symbolic modifications and calling conventions.

We can use VS’s Developer Powershell to compile the above source code.

1
2
cl /c main.c
Link /dll main.obj

The compilation will generate main.dll, a dynamic library that we will use later.

For the above C function, we have the following JS code, assuming it is in the same folder as the C code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* index.js */
const ffi = require('ffi')
const ref = require('ref')
const ref_struct = require('ref-struct')

const t_s = ref_struct({
  a: ref.types.int,
  b: ref.types.char
})

const t_s_ref = ref.refType(t_s)

const test_ffi = ffi.Library(__dirname + '\\main', {
  add_one: ['int', ['int']],
  // aka 'add_one': [ref.types.int, ['int']],
  struct_test: ['void', ['pointer']]
  // aka 'struct_test': ['void', [t_s_ref]],
})
const result = test_ffi.add_one(20)
console.log(result) //21

t_s_p = ref.alloc(t_s_ref)
test_ffi.struct_test(t_s_p)
console.log(t_s_p)
console.log(t_s_p.deref())
console.log(t_s_p.deref().deref()) //a->1, b->'d'

First, a structure t_s , corresponding to the type t_s* in C, is declared in the code. Then, we also get a reference t_s_ref to the structure t_s , which corresponds to the type t_s* in C. Why is there an extra layer of pointers in C? The reason is the same as for the previous string.

Then, the test_ffi variable is declared, which calls the ffi.Library method, which returns the handle to the DLL in JS and the function declaration through which the DLL call can be made. The method takes two arguments, the name of the dynamic library (you can omit the expansion name dll) and an object describing the symbols of the C function and its arguments.

The list has been briefly described above. The key of the object is the name of the function, the value is an array, the first element of which is the type of the function’s return value, and the second element is another array that contains the types of the function’s input parameters. These types use the ref package based type system introduced in the previous section. The types can be represented as strings or as code, see aka’s comments in the code.

Next, the code calls the add_one function in the C dynamic library via test_ffi.add_one, which is called in the same way as the function in JS. However, you need to be careful not to pass the wrong type of arguments, especially since the string type in C is different from the string type in JS and should be converted as mentioned above.

The code then uses ref.alloc to create a memory space for the t_s_ref variable (note that this is a pointer-sized space, not a structure size), assigns the address to the t_s_p variable, and then passes the variable to the struct_test function. Since t_s_p is a duplex pointer, it needs to be dereferenced twice to get the real value of the structure.

Callback functions

A callback function can be declared using the ffi.Callback() function, where the first argument to the function is the return value, the second argument is a list of incoming parameters, and the third argument is the closure of the real callback function.

For example, a callback function is defined as follows, which gets the username and id and returns whether the action was executed successfully.

1
typedef int(*callback)(int, const char*);

Then, in ffi, the callback function can be declared using the following.

1
2
3
4
const callback_function = ffi.Callback('int', ['int', 'string'], (id, username) => {
  // do something
  return 1
})

After the declaration, the callback function needs to be passed into some function as a parameter.

1
2
3
4
5
6
test_ffi.set_a_callback(callback_function)

// Make an extra reference to the callback pointer to avoid GC
process.on('exit', function() {
  callback_function
})

In particular, it is important to make sure that the function has a reference in JS after setting up the callback function (for example, a reference to the function is placed in the NodeJS exit event, which is a classic practice). Otherwise, the function will be destructured by the NodeJS GC. This happens when the program starts executing normally, but when the callback function is called after a while, the program exits abnormally. If you use VS to debug the program, you will see that the program may have accessed an illegal pointer because the DLL code also stores the pointer to the callback function, but in JS the address pointed to by the pointer is not referenced by the code in JS, so it is freed by the CG, so when the code in the DLL calls the function at that address, it accesses illegal memory.

Some Tips

How to debug DLL

One of the biggest problems you may find in using ffi is that it is difficult to debug your program. Especially when dealing with a DLL, it’s like working with a black box operation. Although you have translated its API into JS code using FFI against the header file, it’s still difficult to determine whether the passed or returned values are correct, whether the parameters are correctly passed in the C++ code, whether they are correctly executed after being passed, etc. This requires a way to be able to debug.

A good way to do this is to use the Attach-to-process method of Visual Studio, the number one IDE in the universe for debugging. However, this debugging method requires that you have the source code of the DLL or the PDB (symbolic) file (if you don’t have it, you can only see the disassembled code near the exceptions, which are usually caused by memory errors, and the data near it may not make much sense).

If you have the source files on hand, then first open the project, then after loading the DLL in NodeJS, you can select “Attach to process” when starting the project, and select the NodeJS process in the dialog to enter the debugging interface. In the debug interface, you can insert breakpoints and also see the memory near the breakpoints.

If you don’t have source files on hand but have PDB files (or a small amount of source code), you can use VS to open an empty project and then add the location of symbol files in the debugging settings so that you can also debug with breakpoints. During debugging, you can check whether the code has hit breakpoints or not, and when you hit breakpoints, you will be guided to load the project file, and if you have, you can select it, otherwise you can check the disassembly code near the breakpoints.

You can refer to the MSDN documentation for the specific debugging method: https://docs.microsoft.com/en-us/visualstudio/debugger/attach-to-running-processes-with-the-visual-studio-debugger?view=vs-2019, I won’t go into too much detail here.

How to load a DLL that is in another folder

If the JS file and the DLL file are not in the same folder, the loading may fail with an error like “Dynamic Linking Error: Win32 error 126”.

In this case, you need to put the path of the DLL folder in the PATH where the system is looking for dynamic linking libraries, but FFI does not provide such an interface. However, the Windows API provides the SetDllDirectoryA interface to switch the PATH of the process looking for DLLs, which can be done with the following code.

1
2
3
4
const kernel32_ffi = ffi.Library('kernel32', {
  SetDllDirectoryA: ['bool', ['string']]
})
kernel32_ffi.SetDllDirectoryA(your_custom_dll_directory)

Some linking errors

As mentioned above, if the dynamic library is not in the PATH, the dynamic library will not be found and the error “Dynamic Linking Error: Win32 error 126” will be reported, and in some other cases, the error will be reported whenever the dynamic library is not found. If this error occurs, you need to check if the name of the dynamic library is correct, check if the version of the dynamic library is correct (e.g. 32-bit Node is using a 64-bit DLL), etc.

Another common error is “Dynamic Linking Error: Win32 error 127”, which means that the corresponding symbol is not found in the DLL, so you may need to check if the function name declared in the ffi is correct and if the DLL version is not correct.

Using FFI in Electron

Since each version of Electron is built based on the corresponding Node and Chrome versions, you need to install the local NodeJS version according to the version of Electron you are using before using FFI, otherwise the FFI may not match the Node version, resulting in an ABI version discrepancy: xx was compiled This version of Node.js requires NODE_MODULE_VERSION xx (NodeJS uses NODE_MODULE_VERSION VERSION to identify the ABI version).

In this case, you can use the electron-rebuild mentioned above to recompile all the plugins in the project (note that the local NodeJS ABI version must be the same as the NodeJS ABI version in Electron).

Also, note that versions of Electron above 5 use the NodeJS 12 ABI, but the current Ref library does not support that ABI, which will cause the build to fail. However, a pull request has been submitted to fix this, and I believe there will be a version available.

There are also NAPI versions of FFI and Ref, named ffi-napi and ref-napi respectively, and NAPI versions of ref-related packages, such as array and struct extensions, with the same naming convention as above. The Node C++ extension interface using NAPI is relatively stable and is the future trend.

Finally, the Electron version can be viewed at https://electronjs.org/releases/stable, while the versions of NodeJS and its ABI can be viewed at https://nodejs.org/en/download/releases/.