C++ Add-ons for Node.js v4
NAN, or Native Abstractions for Node.js (a tongue-in-cheek backronym), exists to help ease the complexity of writing C++ add-ons for Node.js that support the changing landscape of the V8 API as it has evolved, and will continue to evolve.
NAN: What and why
Until recently, the 1.x series of NAN releases has been providing support for the full range of maintained Node.js and io.js releases, from Node.js 0.8, 0.10, 0.12 to io.js 1.x and 2.x. This also includes the versions of io.js used in both Electron / Atom and NW.js.
But now, as of io.js v3 and Node.js v4, we have a new series of V8 releases that have introduced new API shifts that are large enough to warrant changing significant parts of NAN. The authors of NAN have taken this chance to do a major rework of the API to move it away from being primarily a set of macros to being a light wrapper layer over newer versions of V8 and an emulation layer over older versions—providing mirrors of new APIs that don't exist in these old versions. So, as of NAN v2, programming against the NAN API is much closer to programming directly against V8 in terms of the patterns and datatypes you have to use.
V8 versions v4.3 (skipped by io.js) and v4.4 (included in io.js v3) and continued in v4.5 (in Node.js v4) introduce some new API features and remove existing ones, and it has become impractical to keep on covering over the changes. NAN's promise is to provide a single API to develop against, not a forever-stable API, it was always anticipated that it would change and adapt to V8 but do so in a way that you get to remain compatible with older versions of V8.
A quick recap of V8 API drift
The dramatic departure from the V8 API that we had become used to for Node.js v0.8 and v0.10 necessitated a compatibility layer for writing add-ons if you wanted to maintain support across actively supported versions of Node.js. What's more, it became clear that V8 was a moving target, with the team willing to break backward-compatibility at relatively short notice and in ways that make it difficult keep up.
Some of the most significant shifts since the somewhat stable days of Node.js v0.8 (V8 v3.11) and Node.js v0.10 (V8 v3.14) as we moved into Node.js v0.12 (V8 3.28) and the early io.js series (V8 3.31 and 4.1+):
- The removal of the
Arguments
object, replacing it with a completely new API for JavaScript-accessible methods around theFunctionCallbackInfo
andPropertyCallbackInfo
classes. Return values are now set on the*CallbackInfo
object rather than returned from the method. - The removal of
Persistent
from theHandle
class hierarchy so that they are no longer easily interchangeable withLocal
handles. - The introduction of
Isolate
as a required argument for the creation of new JavaScript objects and many other APIs.
Fast-forward to io.js v3.x and Node.js v4, we come to V8 v4.4 and v4.5 with some new major changes:
- The deprecation of
Handle
, with its complete removal slated for a coming release. - The introduction of a new
Maybe
type andMaybeLocal
handle to represent values that may or may not exist. These are now used by a number of basic types, giving you additional hoops to jump through to get at their underlying values. - The removal of
SetIndexedPropertiesToExternalArrayData
and related APIs which Node has previously relied upon for itsBuffer
implementation. This has forced a complete rewrite ofBuffer
, now implemented on top ofUint8Array
. Thankfully we had a bit of advance warning from the V8 team although this API change was the primary reason for the io.js v3 delay and the fact that we skipped shipping V8 4.3.
If you're interested in all of the minor changes and a full catalogue of changes in the C++ API, refer to this document which also lists future API changes the V8 team has planned in the short-term.
All of the major changes combined tell a story of the changing ideas of the V8 team regarding what an ideal and safe API is, but it also makes clear that there isn't much appetite for maintaining the kind of API stability that we are used to in JavaScript-land, making it difficult for developers trying to extend Node.js at the native layer.
A change of approach
As mentioned above, NAN started off as a collection of macros and a few helper classes and evolved this approach through v0.x and v1.x. But with new maintainers, particularly Benjamin Byholm, and the complexity of dealing with such broad API changes, NAN v2 is less a macro soup and more a compatibility layer. The new approach taken for NAN v2 is to present an API that is similar to recent versions of V8, providing light wrappers when targeting recent releases of V8 and more complete implementations for older versions.
A prime example of this is the Nan::FunctionCallbackInfo
and Nan::PropertyCallbackInfo
classes and the associated JavaScript-accessible method signatures. In the past you would have to use NAN_METHOD(name)
to implement a JavaScript-accessible method and this in turn would translate to a function taking a v8::Arguments
object for older Node and v8::FunctionCallbackInfo
for newer Node. But now, even though this macro is still available, it's not necessary to use it, as NAN provides its own method signatures that can be applied via Nan::SetMethod()
and related functions. For newer versions of V8, the Nan::*CallbackInfo
classes are lightweight wrappers over the V8 equivalents, while for older versions of V8 they are more complete implements, presenting the new API while interacting with the very different V8 API.
A minimal example
#include <nan.h>
// This method signature magically works from Node.js v0.8 up to
// through io.js to Node.js v4, even though it looks nothing like
// the signature normally required when writing against
// Node.js 0.8 and 0.10.
// It can still be written as NAN_METHOD(Method) if desired.
void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}
NAN_MODULE_INIT(Init) {
// Note the replacement of NanNew() with the namespaced Nan::New().
// The new MaybeLocal API requires ToLocalChecked() for many V8
// types.
v8::Local<v8::Function> helloFn = Nan::GetFunction(
Nan::New<v8::FunctionTemplate>(Method)).ToLocalChecked();
Nan::Set(target, Nan::New("hello").ToLocalChecked(), helloFn);
}
NODE_MODULE(hello, Init)
More practical examples can be found in npm, look for add-ons that are using at least version 2 of NAN. This example from bignum implements the add()
method that can be used to add one BigNum
object to another:
NAN_METHOD(BigNum::Badd)
{
BigNum *bignum = Nan::ObjectWrap::Unwrap<BigNum>(info.This());
BigNum *bn = Nan::ObjectWrap::Unwrap<BigNum>(info[0]->ToObject());
BigNum *res = new BigNum();
BN_add(&res->bignum_, &bignum->bignum_, &bn->bignum_);
WRAP_RESULT(res, result);
info.GetReturnValue().Set(result);
}
Important changes here are the use of Nan::ObjectWrap
instead of node::ObjectWrap
, the use of info
instead of args
which is actually a Nan::FunctionCallbackInfo
mirroring the v8::FunctionCallbackInfo
implementation, hence the new style return value setting at the end of the method. More on all of this below.
Major changes
Project ownership and management
On the non-technical side, the NAN project has been moved into the nodejs organisation on GitHub and is now maintained by the Addon API working group. The repository's new home is https://github.com/nodejs/nan.
The Nan
namespace
Most exports from NAN are exposed in the new Nan
namespace. This was chosen over the more idiomatic nan
due to a conflict with the function of the same name in the commonly used <math.h>
. Even many of the old NAN macros are now types, templates, functions or other elements that can be namespaced. Only a few macros remain global, including:
NAN_METHOD(methodname)
NAN_GETTER(methodname)
NAN_SETTER(methodname)
NAN_PROPERTY_GETTER(methodname)
NAN_PROPERTY_SETTER(methodname)
NAN_PROPERTY_ENUMERATOR(methodname)
NAN_PROPERTY_DELETER(methodname)
NAN_PROPERTY_QUERY(methodname)
NAN_INDEX_GETTER(methodname)
NAN_INDEX_SETTER(methodname)
NAN_INDEX_ENUMERATOR(methodname)
NAN_INDEX_DELETER(methodname)
NAN_INDEX_QUERY(methodname)
NAN_MODULE_INIT(initfunction)
NAN_EXPORT(target, method)
alsoNan::Export(target, name, method)
NAN_GC_CALLBACK(callbackname)
Support for Maybe
types
There are two new classes in V8, MaybeLocal
and Maybe
.
In the words of the V8 header documentation on Maybe
:
A simple
Maybe
type, representing an object which may or may not have a value, see https://hackage.haskell.org/package/base/docs/Data-Maybe.html. If an API method returns aMaybe<>
, the API method can potentially fail either because an exception is thrown, or because an exception is pending, e.g. because a previous API call threw an exception that hasn't been caught yet, or because aTerminateExecution
exception was thrown. In that case, aNothing
value is returned.
So, for many V8 APIs that return primitive types, including bool
, double
, int32_t
, int64_t
, uint32_t
and double
, they will do so now by wrapping it in a Maybe
object. You can check whether there values inside these objects with the obj.IsNothing()
or the opposite, obj.IsJust()
. You can fetch the raw value from Maybe
with obj.FromJust()
but your program will crash if it's actually a Nothing. Alternatively, use the obj.FromMaybe(default_value)
method to fetch a raw value or a default value in the case of a Nothing.
As indicated by the V8 documentation, this concept is inspired by Haskell style monads ... yay ... although you may be best to think of it more akin to a Promise
in that it encapsulates a state and a possible value or error.
Stepping beyond primitives, enter MaybeLocal
:
A
MaybeLocal<>
is a wrapper aroundLocal<>
that enforces a check whether theLocal<>
is empty before it can be used. If an API method returns aMaybeLocal<>
, the API method can potentially fail either because an exception is thrown, or because an exception is pending, e.g. because a previous API call threw an exception that hasn't been caught yet, or because aTerminateExecution
exception was thrown. In that case, an emptyMaybeLocal
is returned.
It's important to note that you only get MaybeLocal
s returned from a limited set of V8 types that were previously returned as simple Local
s, including Array
, Boolean
, Number
, Integer
, Int32
, Uint32
, String
, RegExp
, Function
, Promise
, Script
and UnboundScript
.
A MaybeLocal
has a simple obj.IsEmpty()
method to check whether there is a value inside the Local
. You can retrieve the underlying Local
using the obj.ToLocalChecked()
method but like Maybe#FromJust()
, if the Local
is empty, your program will crash. Also like Maybe
, there is an obj.FromMaybe(default_value)
which you can provide with a new Local
to be used as a default if the MaybeLocal
has an empty Local
.
The reason for introducing this additional abstraction layer (recall that Local
already has an IsEmpty()
method!) according to the V8 team:
... it's important to always assume that API methods can return empty handles. To make this explicit, we will make those API methods return
MaybeLocal<>
instead ofLocal<>
Like a lot of recent changes to the V8 API, this is an attempt to increase safety for V8 embedders.
NAN deals with this new API by providing its own version of these two classes, Nan::Maybe
and Nan::MaybeLocal
. There is also Nan::Nothing
and Nan::Just
. When compiling against newer V8, these are simple wrappers but when compiling against older versions of V8 you get a re-implementation of what you are missing.
An additional step that NAN v2 has taken to accommodate the introduction of the Maybe
types is to expose some utility functions to help deal with V8 APIs that now deal with Maybe
or MaybeLocal
but don't in previous versions. The following functions, so far, have variants in the current V8 API that either return a Maybe
type or accept one as an argument. For maximum portability, opt for using the NAN version.
v8::Value#ToDetailString()
→Nan::ToDetailString()
v8::Value#ToArrayIndex()
→Nan::ToArrayIndex()
v8::Value#Equals()
→Nan::Equals()
v8::Function#NewInstance()
andv8::ObjectTemplate#NewInstance()
→Nan::NewInstance()
v8::FunctionTemplate#GetFunction()
→Nan::GetFunction()
v8::Object#Set()
→Nan::Set()
v8::Object#ForceSet()
→Nan::ForceSet()
v8::Object#Get()
→Nan::Get()
v8::Object#GetPropertyAttributes()
→Nan::GetPropertyAttributes()
v8::Object#Has()
→Nan::Has()
v8::Object#Delete()
→Nan::Delete()
v8::Object#GetPropertyNames()
→Nan::GetPropertyNames()
v8::Object#GetOwnPropertyNames()
→Nan::GetOwnPropertyNames()
v8::Object#SetPrototype()
→Nan::SetPrototype()
v8::Object#ObjectProtoToString()
→Nan::ObjectProtoToString()
v8::Object#HasOwnProperty()
→Nan::HasOwnProperty()
v8::Object#HasRealNamedProperty()
→Nan::HasRealNamedProperty()
v8::Object#HasRealIndexedProperty()
→Nan::HasRealIndexedProperty()
v8::Object#HasRealNamedCallbackProperty()
→Nan::HasRealNamedCallbackProperty()
v8::Object#GetRealNamedPropertyInPrototypeChain()
→Nan::GetRealNamedPropertyInPrototypeChain()
v8::Object#GetRealNamedProperty()
→Nan::GetRealNamedProperty()
v8::Object#CallAsFunction()
→Nan::CallAsFunction()
v8::Object#CallAsConstructor()
→Nan::CallAsConstructor()
v8::Message#GetSourceLine()
→Nan::GetSourceLine()
v8::Message#GetLineNumber()
→Nan::GetLineNumber()
v8::Message#GetStartColumn()
→Nan::GetStartColumn()
v8::Message#GetEndColumn()
→Nan::GetEndColumn()
v8::Array#CloneElementAt()
→Nan::CloneElementAt()
So, it's time to grok Maybe
and accept it as part of your C++. Be sure to stick to the Nan
namespaced versions if you want portable code.
NanNew() → Nan::New()
Nan::New()
is the namespaced version of the old NanNew()
but it's been completely rewritten to be more flexible and be cleverer at matching the types you want to use with it. It's important that you use Nan::New()
to create new JavaScript objects because of the differences in the New()
APIs for various objects across V8 versions since Node.js v0.10, NAN hides all of these discrepancies and will continue to do so.
Additionally it also now supports the new Maybe
types, so where V8 wants to give these to you, you'll get a Nan::MaybeLocal
.
The old functions that return the basic singletons, such as NanUndefined()
have also been namespaced:
Type conversion
Normally you would use obj->ToX()
where X
is a new type that you want to convert to. Perhaps a String
to a Number
. Because this isn't guaranteed to succeed, V8 now uses the Maybe
types to give you a bit of added safety. So, for maximum portability, you should now avoid obj->ToX()
and instead use the Nan::To()
function. Specify the type to get what you want, for example, perhaps you are sure that the first argument of your method is a Number
and you want it as a Local
:
v8::Local<Number> numberArg = Nan::To<v8::Number>(info[0]).ToLocalChecked();
In this case, info[0]
fetches a Local<Value>
and in the past we'd have used info[0].To<Number>()
to convert it, but thanks to the MaybeLocal
in the middle now we have to use Nan::To()
to ensure maximum compatibility. It's important to note here that we are jumping straight to ToLocalChecked()
whereas the intention of the creators of MaybeLocal
was that we'd first check if it was empty because doing so without first checking will crash your program. So beware.
Errors
Yep, v8::TryCatch
now interacts with Maybe
types, it was also modified to take an Isolate
argument, so there is now a Nan::TryCatch
to address this where you need it.
NAN has also moved its old error creation and throwing utilities to new namespaced functions. These can be used for maximum Node version compatibility and also simpler use of V8's exceptions. e.g. Nan::ThrowTypeError("Pretty simple to throw an error");
.
Nan::Error()
Nan::RangeError()
Nan::ReferenceError()
Nan::SyntaxError()
Nan::TypeError()
Nan::ThrowError()
Nan::ThrowRangeError()
Nan::ThrowReferenceError()
Nan::ThrowSyntaxError()
Nan::ThrowTypeError()
Nan::FatalException()
Nan::ErrnoException()
Buffers
Interacting with Buffer
s is important for most compiled addons as it's a primary type for passing around binary data with Node.js. The new namespaced versions of buffer creation functions are:
Nan::NewBuffer()
: Use this if you are handing off an existingchar*
to be owned and managed by the newBuffer
. This is the most efficient way of creating a buffer but it means you have to have full confidence that the ownership of that area of memory can be safely handed-off. This is often not the case, e.g. third-party libraries that manage their own memory.Nan::CopyBuffer()
: Use this where you need Node to make a copy of the data you're providing. This is obviously slower than re-using the existing memory but also the safest if you don't have full control over thechar*
you are passing.
An example of this can be found in LevelDOWN, where the LevelDB instance in use is responsible for managing the underlying data extracted from the data store so LevelDOWN has to resort to making a copy of it:
v8::Local<v8::Value> returnValue;
if (asBuffer) {
// TODO: could use NewBuffer if we carefully manage the lifecycle of
// `value` and avoid an an extra allocation. We'd have to clean up
// properly when not OK and let the new Buffer manage the data when OK
returnValue = Nan::CopyBuffer(
(char*)value.data(), value.size()).ToLocalChecked();
} else {
returnValue = Nan::New<v8::String>(
(char*)value.data(), value.size()).ToLocalChecked();
}
As per the TODO
, it would be ideal if LevelDOWN could take responsibility for the original char*
and avoid the memcpy()
that happens when you call CopyBuffer()
.
There is also Nan::FreeCallback
that can be used to define a callback function that is passed to Nan::NewBuffer()
if you need a particular action to be taken when the Buffer
hits the garbage collector. By default, the memory is freed with free
, if this is not going to work with what you have provided NewBuffer()
then implement a custom FreeCallback
function. If you are passing a pointer to static memory, then provide an empty function, if you are passing something created with new
then implement a function that uses delete
. Nan::FreeCallback
is necessary because node::smalloc::FreeCallback
had to be moved to node::Buffer::FreeCallback
for io.js v3 and Node.js v4 when the smalloc
module was removed from core.
Asynchronous work helpers
Nan::AsyncWorker
and Nan::AsyncProgressWorker
are helper classes that make working with asynchronous code easier. AsyncWorker
was in NAN from the begining (as NanAsyncWorker
) but AsyncProgressWorker
came in v1.4. It works like AsyncWorker
except it doesn't just have a single return point back to JavaScript, it can post on-going updates to JavaScript as the work progresses.
Additionally don't forget about Nan::Callback
for managing callbacks over the life of an asynchronous execution. This helper, formally NanCallback
primarily exists to ensure that the callback Function
remains free from garbage collection while you are waiting for asynchronous execution to return.
Some examples of how Nan::AsyncWorker
and Nan::Callback
can be used to simplify working with V8 in an asynchronous environment can be found scattered through LevelDOWN, simply look through the *<i>async.cc
files in the src
directory.
Encodings and V8 internals
NAN v2 namespaces its encoding/decoding functions for dealing with strings and bytes, see the documentation for more details.
Nan::Utf8String
should now be used in place of v8::String::Utf8Value
to get its more recent functionality enhancements that are not present in past versions of Node.
Dealing with V8 internals have also been namespaced and expanded to deal with changing argument and return types as well as wholesale renaming of APIs. See the documentation for more details.
The _current Context
should now be accessed via Nan::GetCurrentContext()
. Interact with Isolate
using Nan::SetIsolateData()
and Nan::GetIsolateData()
.
Node.js helpers
Instead of opting for the pure Node.js versions of the following APIs, you should use the NAN implementations for maximum version compatibility:
node::MakeCallback()
→Nan::MakeCallback()
node::ObjectWrap
→Nan::ObjectWrap
Also, use NAN_MODULE_INIT()
to define an "Init
" function for Node.js add-ons due to the change from Handle
to Local
. Generally you'll be writing NAN_MODULE_INIT(Init) { /* ... export things here on 'target' ... */ }
. Note that in older versions of Node.js you'll receive a Handle
rather than a Local
, this may require some massaging to get full compatibility. It's also common to have multiple init functions in a non-trivial add-on as various components need to register their exports. Such as this code from node-canvas:
NAN_MODULE_INIT(init) {
Canvas::Initialize(target);
Image::Initialize(target);
ImageData::Initialize(target);
Context2d::Initialize(target);
Gradient::Initialize(target);
Pattern::Initialize(target);
#ifdef HAVE_FREETYPE
FontFace::Initialize(target);
#endif
...
Within an init function you should use Nan::Export()
to attach properties / functions to target
(a.k.a exports
), this deals with both the Handle
vs Local
difference as well as managing some of the MaybeLocal
mess for you.
What next?
NodeSource is now providing an upgrade-utils package via npm. Install it with npm install upgrade-utils -g
then move to the root directory of your Node.js package and run upgrade-utils
. See the documentation for more information on usage options.
The upgrade-utils
utility is designed to help with the process of upgrading your packages to Node.js v4 and one of it's features is that it can do 90% of the work of converting your C++ code from NAN v1 to NAN v2. It's not all automatic, however, you'll need to pay attention to:
- brace and bracket confusion because these are just regexes.
- missing returns where NAN v1 might have had an implicit returns, e.g. NanReturnNull()
and NanReturnValue()
which should now simply use info.GetReturnValue().Set()
however are not accompanied by a return
in the script.
Note that upgrade-utils
doesn't just do C++, try it against your existing Node.js modules to see what it comes up with!
Get in touch: file an issue on the NAN repository if you are struggling with NAN v2 and have questions. Or, if you have in-house C++ add-ons and need some professional help, feel free to contact NodeSource and find out how we can help.