How to create js binding to c/c++ library using nodejs FFI
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.
- Using Node.JS pure addon API
- 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.
- On Linux:
GCC
,make
,python 2.7
- by default, all of these will be installed on most of the linux system. - On Windows:
Visual C++ build Tools
,python 2.7
- you can install all of these usingnpm install --global --production --add-python-to-path windows-build-tools
. To know more about this tool visit this link. - On macOS:
xcode
,python 2.7
- by default, python is installed on macOS. Installxcode
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.