Building and Embedding Node.js as a Shared Library

Much has been written about creating Node.js native addons that allow C/C++ libraries to be called from Node.js applications. This article looks at the reverse process of embedding Node.js in a C++ application. Specifically, this post will teach you how to build Node.js as a shared library and use it in a small C++ wrapper application.

Prerequisites: This post assumes that the reader is running macOS. The shared library will be a .dylib file. This would be a .dll file on Windows, and a .so file on other operating systems. Readers will also be required to build Node.js from source. This is because the Node project does not distribute shared libraries at this point in time. g++ is also used to compile the final example. There is nothing stopping a determined reader from porting this tutorial to another operating system or toolchain.

Download the Node.js Source Code and Header Files

In a web browsers, navigate to http://nodejs.org/dist/latest-v7.x/. Download the source code (node-v7.x.x.tar.gz) and header files (node-v7.x.x-headers.tar.gz), substituting v7.x.x with the actual version number. Once downloaded, extract the source code and headers from their archive files.

Build the Shared Library

Navigate to the directory containing the Node.js source code. Issue the following command to build the shared library.

./configure --shared; make

This command will take a while to execute. By working with a shared library, you can save others the time taken to compile Node (which includes V8 and several other dependencies). Once the build completes, the shared library file will be available in the out/Release subdirectory. For example, on macOS, the file will be out/Release/libnode.51.dylib. The 51 in the filename corresponds to the NODE_MODULE_VERSION, which is important for determining compatibility with Node.js addons. Copy the shared library file to the directory that you plan to complete the rest of this tutorial from (for example /tmp).

Write the C++ Application

In the working directory (/tmp), create a file named embed.cc. Add the following code to embed.cc. This file #includes <stdio.h>, as well as the node.h header file that we previously downloaded. When the main() function runs, it simply prints a message using printf(), and then starts an embedded instance of the Node runtime. Note that argc and argv are passed to Node. This means that the example application supports all of the same command line arguments as a normal Node executable.

#include <stdio.h>
#include "node.h"

int main(int argc, char* argv[]) {  
  printf("Hello Embedded Node.js!\n");
  node::Start(argc, argv);
  return 0;
}

Setup the Header Files

In order to build against node.h, the compiler needs to know where to look for it. For the purposes of this tutorial, move the extracted headers to your working directory (/tmp). This directory should now contain embed.cc, libnode.51.dylib, and the node-v7.x.x directory that contains the headers.

Compile

Compile the project using the command shown below.

g++ -std=c++11 -L. -lnode.51 -I./node-v7.x.x/include/node embed.cc -o embed  

This command does a few things:

  • Compiles using g++.
  • Enables C++11 language extensions (-std=c++11).
  • Tells g++ to look for libraries in the current directory (-L.).
  • Tells g++ to use the node.51 library (-lnode.51).
  • Tells g++ to search the headers directory for #includes (-I./node-v7.x.x/include/node).
  • Sets embed.cc as the source file to be compiled.
  • Writes the compiled executable to embed (-o embed).

macOS Fix

At this point, the embed executable file should have been created. However, if you try to execute it on macOS, you'll get an error like the one shown below.

$ ./embed
dyld: Library not loaded: /usr/local/lib/libnode.51.dylib  
  Referenced from: /private/tmp/./embed
  Reason: image not found
Abort trap: 6  

The reason for this error is that .dylib files have what is known as an install name. This is a hard coded path inside of the shared library. This path gets hard coded into the embed executable so that it knows where to find the library at runtime. You can see the library's install name using the following command.

$ otool -D libnode.51.dylib
libnode.51.dylib:  
/usr/local/lib/libnode.51.dylib

You can also verify that the same path is in the executable using the following command.

$ otool -L embed
embed:  
    /usr/local/lib/libnode.51.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 307.4.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.0.0)

This output indicates that the install name is /usr/local/lib/libnode.51.dylib. Copying your local libnode.51.dylib to the /usr/local/lib directory will make the embed executable function properly. However, a better solution is to change the install name in the executable using the following command.

$ install_name_tool -change /usr/local/lib/libnode.51.dylib @executable_path/libnode.51.dylib embed

This tells embed to load the library from the current directory. You can verify this using the following command.

$ otool -L embed
embed:  
    @executable_path/libnode.51.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 307.4.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.0.0)

Execute

You should now be able to execute embed as if it were a normal Node.js executable:

$ ./embed --version
Hello Embedded Node.js!  
v7.4.0  

Notice that the custom greeting is displayed before running Node.

Conclusion

This article showed you how to embed Node.js as a shared library inside of a trivial C++ application. As Node.js itself is a C++ application, this might not seem extremely useful. However, it is possible to embed shared libraries in applications written in other programming languages, such as Java. Additionally, by working with a shared library, we have eliminated the need for other users to compile Node.js from source.

The source code for this tutorial is available on GitHub.