A first project
Before you start, make sure you’ve got all the necessary prerequisites and tools installed.
As you select where to begin, you should be aware that Node-API operates at two levels which we can think of as the “C level” and the “C++ level”.
The “C level” code is built entirely into Node itself and is very well documented on the Node documentation pages. If you need low-level access to the intricacies of Node, this is the tool for you.
Alternatively, there is the node-addon-api package which adds a C++ wrapper to the Node-API code built into Node. This package makes working with Node-API much easier as it implements a very nice object model and abstracts away much of the detailed coding that would otherwise be required, while retaining the Node-API promise of ABI stability and forward compatibility.
This tutorial uses node-addon-api
.
Node-API has been in public release and active development starting with Node 8.0.0. Since then, it’s undergone a number of refinements. This tutorial has been tested with Node 10.10.0 and is known to fail with older versions of Node. You will need a copy of Node that supports Node-API in order to develop and run Node-API code. To see which versions of Node support Node-API, refer to the Node-API Version Matrix. You can determine the version of Node you’re running with the command
node -v
.
Creating a project
The easiest way to create a new Node-API project is to use the generator-napi-module
package. As the package documentation describes, generator-napi-module
relies on Yeoman which must also be installed:
npm install -g yo
npm install -g generator-napi-module
On some systems, you may receive the error Error: EACCES: permission denied, access
. In that case, on Mac and Linux systems you need to run the commands with elevated privileges:
sudo npm install -g yo
sudo npm install -g generator-napi-module
Using
nvm
is an excellent way to banish permission issues.
Then enter these commands to generate a new project:
mkdir hello-world
cd hello-world
yo napi-module
Here are the prompts you’ll see and some suggested responses:
package name: (hello-world)
version: (1.0.0)
description: A first project.
git repository:
keywords:
author: Your name goes here
license: (ISC)
Yeoman will display the generated package.json
file here.
Is this OK? (yes) yes
? Choose a template Hello World
? Would you like to generate TypeScript wrappers for your module? No
Yeoman will now build your “Hello World” add-on module.
At this point, you might try running npm test
to make sure everything is correctly installed:
npm test
Project structure
At this point you have a completely functional Node-API module project. The project files are structured according to Node-API best practices. It should look like this:
.
├── binding.gyp Used by gyp to compile the C code
├── build The intermediary and final build products
│ └── < contents not shown here >
├── lib The Node-API code that accesses the C/C++ binary
│ └── binding.js
├── node_modules Node modules required by your project
│ └── < contents not shown here >
├── package.json npm description of your module
├── package-lock.json Used by npm to insure deployment consistency
├── src The C/C++ code
│ └── hello_world.cc
└── test Test code
└── test_binding.js
Let’s take a look at the essential files.
package.json
{
"main": "lib/binding.js",
"private": true,
"dependencies": {
"node-addon-api": "^8.1.0"
},
"scripts": {
"test": "node --napi-modules ./test/test_binding.js"
},
"gypfile": true,
"name": "hello-world",
"version": "1.0.0",
"description": "A first project.",
"author": "Your name goes here",
"license": "ISC"
}
This is a typical package.json
file as generated by Yeoman from the responses we entered earlier to the yo napi-module
command. There are a couple of entries of interest here.
Notice the node-addon-api
dependency. This package, which is not strictly a part of Node, adds a C++ wrapper to the C API implemented in Node. The package makes it very straightforward to create and manipulate JavaScript objects inside C++. The package is useful even if the underlying library you’re accessing is in C.
There is also a "gypfile": true
entry which informs npm that your package requires a build using the capabilities of the node-gyp
package which is covered next.
binding.gyp
{
'targets': [
{
'target_name': 'hello-world-native',
'sources': [ 'src/hello_world.cc' ],
'include_dirs': ["<!@(node -p \"require('node-addon-api').include\")"],
'dependencies': ["<!(node -p \"require('node-addon-api').gyp\")"],
'cflags!': [ '-fno-exceptions' ],
'cflags_cc!': [ '-fno-exceptions' ],
'xcode_settings': {
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
'CLANG_CXX_LIBRARY': 'libc++',
'MACOSX_DEPLOYMENT_TARGET': '10.7'
},
'msvs_settings': {
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
}
}
]
}
One of the challenges of making C/C++ code available to Node is getting the code compiled, linked, and packaged for a variety of operating systems and architectures. Historically, this would require learning the intricacies of a variety of build tools across a number of operating systems. This is the specific issue GYP seeks to address.
Using GYP permits having a single configuration file that works across all platforms and architectures GYP supports. (It’s GYP, by the way, that requires Python).
node-gyp is a command line tool built in Node that orchestrates GYP to compile your C/C++ files to the correct destination. When npm sees the "gypfile": true
entry in your package.json
file, it automatically invokes its own internal copy of node-gyp
which looks for this binding.gyp
file which must be called binding.gyp
in order for node-gyp to locate it.
The binding.gyp
file is a GYP file which is thoroughly documented here. There is also specific information about building libraries here.
src/hello_world.cc
#include <napi.h>
using namespace Napi;
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "HelloWorld"),
Napi::Function::New(env, Method));
return exports;
}
NODE_API_MODULE(addon, Init)
This is perhaps the simplest useful(?) Node-API file you can have.
The file defines a C++ Method
function that takes a single Napi::CallbackInfo&
argument. This info
argument is used to access the JavaScript environment, including any JavaScript arguments that might be passed in.
info
is an array of JavaScript arguments.
In this case, the C++ Method
function uses the info
argument to create a Napi::Env
local that is then used to create a Napi::String
object which is returned with the value “world”.
The C++ Init
function shows how to set a single export value for the native add-on module. In this case the name of the export is “HelloWorld” and the value is the Method
function.
The NODE_API_MODULE
macro at the bottom of the C++ file insures that the Init
function is called when the module is loaded.
lib/binding.js
const addon = require('../build/Release/hello-world-native');
module.exports = addon.HelloWorld
This JavaScript file defines a JavaScript class that acts as an intermediary to the C++ binary.
In this case, the sole export of the binding is the HelloWorld
function.
test/test_binding.js
const HelloWorld = require("../lib/binding.js");
const assert = require("assert");
assert(HelloWorld, "The expected function is undefined");
function testBasic()
{
const result = HelloWorld("hello");
assert.strictEqual(result, "world", "Unexpected value returned");
}
assert.doesNotThrow(testBasic, undefined, "testBasic threw an expection");
console.log("Tests passed- everything looks OK!");
This code demonstrates how to load and call the HelloWorld
function using JavaScript. Recall that the sole export from the binding is the HelloWorld
function. The function is loaded into the HelloWorld
variable using the require
command.
The testBasic
function then calls the HelloWorld
function and verifies the result.
Conclusion
This project demonstrates a very simple Node-API module that exports a single function. In addition, here are some things you might want to try:
- Run
test_binding.js
in your debugger. See if you can step through the code to get a better understanding of how it works. What sort of visibility are you getting into the JavaScript object created by the C++ code? - Modify
test_binding.js
to use the C++ binary module directly instead of throughbinding.js
. Step through the code in your debugger to see how things are different. - Modify
hello_world.cc
to access arguments passed from the JavaScript. Hint: Thenode-addon-api
module comes with examples.
Resources
- node-addon-api Documentation
- The generator-napi-module Package
- The node-gyp Package
- GYP and .gyp file Documentation.
- Yeoman