Sysleaf

How to create js binding to c/c++ library using nodejs FFI

. .
How to create js binding to c/c++ library using nodejs FFI

Introduction

If you have an existing library written in C/C++ and you want to use this in a Node.JS application or you want to create an interface binding for javascript. There are two ways to integrate C/C++ library code in Node.JS. Both has its own set of advantage & disadvantage.

  1. Using Node.JS pure addon API
  2. Using Node.JS addon ‘FFI’
Node.Js addon FFI
It is Node.JS pure inbuilt addon API It is external module written itself using Node.JS addon API
Good knowledge of reading & writting of C/C++ is required Basic knowledge of reading C/C++ is required
You will have to write C/C++ code to create binding You don’t have to write a single C/C++ code to create binding

In this tutorial, we will write a simple C/C++ math library and learn how to integrate it in Node.JS using FFI. FFI (foreign fetch interface) is a Node.JS addon/module for loading and calling dynamic/shared library using pure Javascript.

Write C/C++ library

If you already have C/C++ library then you can skip to section: writing JS binding to C/C++ library. In this section, we will create a simple math library which will have three functions add, minus and multiply.

Generally, standalone application will have main() function. Since this is a library, it will not have main() function. Libraries are of two types: 1. Static Library and 2. Dynamic/Shared Library.

Static Library: is a reusable binary code that is linked to program at compile time. For example, any program using the static library will copy the code of static library in its own code at compile time. It means, the program will not fail at runtime if the static library is not present in the system. Static library increases the size of a program. The file extension of a static library is .lib on windows and .a on linux and macOS.

Dynamic/Shared Library: is a reusable binary code that is invoked by a program at runtime. For example, any program using the dynamic library will include only the reference of it at compile time, actual code of dynamic library will be called at runtime of the program. It means program will fail at runtime if the dynamic library is not present in the system. Dynamic library does not increase the size of program. File extension of dynamic library is .dll on windows, .so on linux, and .dylib on macOS.

FFI supports only shared library, So in the subsequent section, our focus will be on shared library only. Now see below math related function in math.c. In order to make these function sharable, we will have to provide an extra declaration, which you can see in math.h file. We will have to give proper declaration (see math.h) in order to compile function as a shared library, it is platform dependent.

  • #ifdef __linux__ 
      extern "C" int add(int x, int y);
      extern "C" int minus(int x, int y);
      extern "C" int multiply(int x, int y);
    #elif _WIN32
      extern "C" __declspec(dllexport) int add(int x, int y);
      extern "C" __declspec(dllexport) int minus(int x, int y);
      extern "C" __declspec(dllexport) int multiply(int x, int y);
    #elif __APPLE__
      extern "C" int add(int x, int y);
      extern "C" int minus(int x, int y);
      extern "C" int multiply(int x, int y);
    #endif
    
  • #include "math.h"
    
    int add(int x, int y)
    {
      return x + y;
    }
    
    int minus(int x, int y)
    {
      return x - y;
    }
    
    int multiply(int x, int y)
    {
      return x * y;
    }
    

Compile & Build C/C++ library

To compile our C/C++ code and build a shared library out of it, we will use build tool called node-gyp. node-gyp is cross-platform command line tool written in Javascript for compiling native addon for Node.JS. Generally, each platform (windows, linux, macos etc) has different build process and different build tool. To avoid the pain of dealing with the differences in the build process of each platform, it is imperative to use a cross-platform build tool like node-gyp.

we only have to deal with node-gyp, node-gyp internally uses platform dependent build tool and deal with its difference. So we still need to have those tool already installed before installing node-gyp. See below for platform specific build tool that node-gyp requires. To know more about node-gyp visit this link.

  1. On Linux: GCC, make, python 2.7 - by default, all of these will be installed on most of the linux system.
  2. On Windows: Visual C++ build Tools, python 2.7 - you can install all of these using npm install --global --production --add-python-to-path windows-build-tools. To know more about this tool visit this link.
  3. On macOS: xcode, python 2.7 - by default, python is installed on macOS. Install xcode from App app store or download it from here.

Now to install node-gyp globally, type command: npm install node-gyp -g.

Now, how the node-gyp will know: what all files to compile? where is all input C/C++ file? what will be the name of output library? This is where the binding.gyp file comes into the picture. node-gyp uses this file defines its build behavior. So we put our all source and build related information into this file. Below is the content of binding.gyp file.

{
  "targets": [
    {
      "target_name": "math",
      "type": "shared_library",
      "sources": [ "math.cc" ]
    }
  ]
}

In above code, sourcs is the list of input files (C/C++ source). target_name is the name of output file(.so .dll etc). type determine nature of output file. Generally type will be: shared_library for .so & .dll like file, static_library for .a & .lib like file, executable for .out & .exe like file. See this link to know more about node-gyp configuration.

Now to build the shared library, run below command from the folder where files binding.gyp, math.cc, and math.h is present. It will generate appropriate file (.so, .dll, or .dylib) in new folder build/Release.

node-gyp clean configure build or node-gyp rebuild

To see the symbol entry of shared library, use below platform dependent command.
On Windows: dumpbin /exports math.dll
On Linux: nm -gC math.so or objdump -TC math.so
On MacOS: nm -gU math.dylib

Write JS binding to C/C++ library

Now we have created our own basic math shared library (.dll, .so, or .dylib file) which is written in C/C++. Now we want to use/call a function inside this library as a normal Javascript API because we don’t want to write another library in Javascript which is already written in C/C++.

To do this, we will have to create a Javascript wrapper/binding of this C/C++ library using Node.JS addon ffi. Install ffi and save it as a dependency in your package.json by using the command: npm install ffi --save. But before installing ffi make sure you have installed appropriate build tool for your platform as mentioned in the previous section.

Now let’s create a Javascript binding for our math library. See below code on how to do this. As you can see, first we are importing module ffi and ref. ref module is used to map C/C++ data type to Javascript data type. Next, we are storing the name and location of the library in variable mathlibLoc. Name of the library will vary as per the platform, so you can see the platform logic here.

var ffi = require('ffi');
var ref = require('ref');
var int = ref.types.int;

var platform = process.platform;
var mathlibLoc = null;

if (platform === 'win32'){
    mathlibLoc = './math.dll';
}else if(platform === 'linux'){
    mathlibLoc = './math.so';
}else if(platform === 'darwin'){
    mathlibLoc = './math.dylib'
}else{
    throw new Error('unsupported plateform for mathlibLoc')
}

var math = ffi.Library(mathlibLoc, {
    "add": [int, [int, int]],
    "minus": [int, [int, int]],
    "multiply": [int, [int, int]]
});

module.exports = math;

The main function of ffi module is Library() which is used to create a wrapper. In this function, we pass a declaration of all the C/C++ function present in math.[dll/so/dylib] file. ffi.Library() will automatically create a mapping and return a object that will have all the C/C++ function as normal Javascript method.

We are storing this returned object in variable math and later on, we are exporting this object using module.exports=math. Hence our wrapper/binding for C/C++ function is ready as a normal node.js module. Now we can require this module like normal Node.JS module and use it, we will see this in next section. Anyway below is the signature of ffi.Library function.

ffi.Library(libraryFile, { functionSymbol: [ returnType, [ arg1Type, arg2Type, ... ], ... ]);

Using the binding

In the previous section, we have created Javascript binding for C/C++ function as a node.js module called ‘math’. Now, lets use that module. See below code on how to use our math module. As you can see, we are requiring math module like normal Node.JS module and calling its function add, minus, and multiply.

var math = require('./math')
var result = null;

result = math.add(5, 2);
console.log('5+2='+result);

result = math.minus(5, 2);
console.log('5-2='+result);

result = math.multiply(5, 2);
console.log('5*2='+result);

As you know, these functions are not defined in Javascript, they are defined in C/C++ which is present in math.[dll/so/dylib] file. So see how it is easy to use/integrate shared library in Node.JS using ffi module. The end user of our math module will not fill that he is using platform native library instead, it will appear as normal Node.JS module to him.

Reference

  1. see the full source code on github
  2. ffi github page
  3. ffi official tutorial page
  4. ref github page
  5. node-gyp github page