24 KiB
Addon Tools
This is a part of Node3D project.
npm i -s addon-tools-raub
Synopsis
Helpers for Node.js addons and dependency packages:
- C++ macros and shortcuts.
consoleLog()
C++ helper.eventEmit()
C++ helper.getData()
C++ helper.- Crossplatform commands for GYP:
cp
,rm
,mkdir
. - Supported platforms (x64): Windows, Linux, OSX.
Useful links: N-API Docs, Napi Docs, GYP Docs.
Contents
Snippets
binding.gyp
Crossplatform commands
'variables': {
'rm' : '<!(node -e "require(\'addon-tools-raub\').rm()")',
'cp' : '<!(node -e "require(\'addon-tools-raub\').cp()")',
'mkdir' : '<!(node -e "require(\'addon-tools-raub\').mkdir()")',
},
On both Windows and Unix those are the console commands for various file system operations. No need for GYP conditions, yay!
Addon binary directory
'variables': {
'binary' : '<!(node -e "require(\'addon-tools-raub\').bin()")',
},
Include directories
'include_dirs': [
'<!@(node -p "require(\'addon-tools-raub\').include")',
],
Those are the directory paths to C++ include files for Addon Tools and Napi (which is preinstalled with Addon Tools)
Binary dependency package
If you design a module with binary dependencies for several platforms, Addon Tools would encourage you to abide by the following rules:
-
Your binary directories are:
- bin-windows
- bin-linux
- bin-osx
-
The following piece of code in your
index.js
without changes. Methodpaths()
is described here.module.exports = require('addon-tools-raub').paths(__dirname);
Show binding.gyp
{ 'variables': { 'rm' : '<!(node -e "require(\'addon-tools-raub\').rm()")', 'rem' : '<!(node -e "require(\'.\').rem()")', 'XALL%': 'false', }, 'targets': [ { 'target_name' : 'remove_extras', 'type' : 'none', 'conditions' : [['XALL=="false"', {'actions': [ { 'action_name' : 'Unnecessary binaries removed.', 'inputs' : [], 'outputs' : ['build'], 'action' : ['<(rm)', '-rf', '<@(rem)'], } ]}]], } ] }
Notice the
XALL
variable here. If the package is installed withnpm i
, then quite expectedly all but the required arch directories are removed. But withnpm i --XALL
you can keep all the binaries. It might be useful when debugging multiple archs and switching Node.js versions with NVM.
Compiled addon
It is easy to build a C++ addon with Addon Tools. To have a full picture, you can view the official example.
The main file for an addon is binding.gyp. Here's a snippet with most of the features.
binding.gyp
- Assume
EXT_LIB
is the name of a binary dependency. - Assume
deps-EXT_LIB
is the name of an Addon Tools compliant dependency module. - Assume
MY_ADDON
is the name of this addon's resulting binary. - Assume C++ code goes to
cpp
directory.
{
'variables': {
'rm' : '<!(node -e "require(\'addon-tools-raub\').rm()")',
'cp' : '<!(node -e "require(\'addon-tools-raub\').cp()")',
'mkdir' : '<!(node -e "require(\'addon-tools-raub\').mkdir()")',
'binary' : '<!(node -e "require(\'addon-tools-raub\').bin()")',
'EXT_LIB_include' : '<!(node -e "require(\'deps-EXT_LIB\').include()")',
'EXT_LIB_bin' : '<!(node -e "require(\'deps-EXT_LIB\').bin()")',
},
'targets': [
{
'target_name': 'MY_ADDON',
'sources': [
'cpp/MY_ADDON.cpp',
],
'include_dirs': [
'<!(node -e "require(\'addon-tools-raub\').include()")',
'<(EXT_LIB_include)',
'<(module_root_dir)/include',
],
'library_dirs': [ '<(EXT_LIB_bin)' ],
'conditions': [
[
'OS=="linux"',
{
'libraries': [
'-Wl,-rpath,<(EXT_LIB_bin)',
'<(EXT_LIB_bin)/libEXT_LIB.so',
],
}
],
[
'OS=="mac"',
{
'libraries': [
'-Wl,-rpath,<(EXT_LIB_bin)',
'<(EXT_LIB_bin)/EXT_LIB.dylib',
],
}
],
[
'OS=="win"',
{
'libraries': [ 'EXT_LIB.lib' ],
'defines' : [
'WIN32_LEAN_AND_MEAN',
'VC_EXTRALEAN'
],
'msvs_version' : '2013',
'msvs_settings' : {
'VCCLCompilerTool' : {
'AdditionalOptions' : [
'/O2','/Oy', # Comment this for debugging
# '/Z7', # Unomment this for debugging
'/GL','/GF','/Gm-','/EHsc',
'/MT','/GS','/Gy','/GR-','/Gd',
]
},
'VCLinkerTool' : {
'AdditionalOptions' : ['/OPT:REF','/OPT:ICF','/LTCG']
},
},
}
],
],
},
{
'target_name' : 'make_directory',
'type' : 'none',
'dependencies' : ['MY_ADDON'],
'actions' : [{
'action_name' : 'Directory created.',
'inputs' : [],
'outputs' : ['build'],
'action': ['<(mkdir)', '-p', '<(binary)']
}],
},
{
'target_name' : 'copy_binary',
'type' : 'none',
'dependencies' : ['make_directory'],
'actions' : [{
'action_name' : 'Module copied.',
'inputs' : [],
'outputs' : ['binary'],
'action' : ['<(cp)', 'build/Release/MY_ADDON.node', '<(binary)/MY_ADDON.node'],
}],
},
{
'target_name' : 'remove_extras',
'type' : 'none',
'dependencies' : ['copy_binary'],
'actions' : [{
'action_name' : 'Build intermediates removed.',
'inputs' : [],
'outputs' : ['cpp'],
'conditions' : [
[ 'OS=="linux"', { 'action' : [
'rm',
'<(module_root_dir)/build/Release/obj.target/MY_ADDON/cpp/MY_ADDON.o',
'<(module_root_dir)/build/Release/obj.target/MY_ADDON.node',
'<(module_root_dir)/build/Release/MY_ADDON.node'
] } ],
[ 'OS=="mac"', { 'action' : [
'rm',
'<(module_root_dir)/build/Release/obj.target/MY_ADDON/cpp/MY_ADDON.o',
'<(module_root_dir)/build/Release/MY_ADDON.node'
] } ],
[ 'OS=="win"', { 'action' : [
'<(_del)',
'<(module_root_dir)/build/Release/MY_ADDON.*',
'<(module_root_dir)/build/Release/obj/MY_ADDON/*.*'
] } ],
],
}],
},
]
}
Then require the built module like this:
const { binPath } = require('addon-tools-raub');
const core = require(`./${binPath}/MY_ADDON`);
include/addon-tools.hpp
There is a C++ header file, addon-tools.hpp
, shipped with this package. It
introduces several useful macros and utilities. Also it includes Napi automatically,
so that you can replace:
#include <napi.h>
with
#include <addon-tools.hpp> // or event-emitter.hpp
In gyp, the include directory should be set for your addon to know where to get it. As it was mentioned above, this can be done automatically. Also an actual path to the directory is exported from the module and is accessible like this:
require('addon-tools-raub').include // a string
Currently, there are following helpers in addon-tools.hpp:
Handle scope
NAN_HS
- creates a HandleScope. Also, you do not need them withinNAN_METHOD
,NAN_SETTER
, andNAN_GETTER
, as it is stated in Nan doc. So it is most likely to be used in parts of code called from C++ land.
void windowFocusCB(GLFWwindow *window, int focused) { NAN_HS;
...
}
...
glfwSetWindowFocusCallback(window, windowFocusCB);
Method return
RET_VALUE(VAL)
- set method return value, whereVAL
isv8::Local<v8::Value>
.RET_UNDEFINED
- set method return value asundefined
.RET_STR(VAL)
- set method return value, whereVAL
isconst char *
.RET_UTF8(VAL)
- set method return value, whereVAL
isconst char *
.RET_INT(VAL)
- set method return value, whereVAL
isint32
.RET_INT32(VAL)
- set method return value, whereVAL
isint32
.RET_UINT32(VAL)
- set method return value, whereVAL
isuint32
.RET_NUM(VAL)
- set method return value, whereVAL
isdouble
.RET_OFFS(VAL)
- set method return value, whereVAL
issize_t
.RET_FLOAT(VAL)
- set method return value, whereVAL
isfloat
.RET_DOUBLE(VAL)
- set method return value, whereVAL
isdouble
.RET_EXT(VAL)
- set method return value, whereVAL
isvoid *
.RET_BOOL(VAL)
- set method return value, whereVAL
isbool
.RET_FUN(VAL)
- set method return value, whereVAL
isNan::Persistent<v8::Function>
.RET_OBJ(VAL)
- set method return value, whereVAL
isNan::Persistent<v8::Object>
.
Shortcut types
V8_VAR_VAL
=v8::Local<v8::Value>
V8_VAR_OBJ
=v8::Local<v8::Object>
V8_VAR_ARR
=v8::Local<v8::Array>
V8_VAR_STR
=v8::Local<v8::String>
V8_VAR_FUNC
=v8::Local<v8::Function>
V8_VAR_FT
=v8::Local<v8::FunctionTemplate>
V8_VAR_OT
=v8::Local<v8::ObjectTemplate>
V8_STORE_FT
=Nan::Persistent<v8::FunctionTemplate>
V8_STORE_FUNC
=Nan::Persistent<v8::Function>
V8_STORE_OBJ
=Nan::Persistent<v8::Object>
V8_STORE_VAL
=Nan::Persistent<v8::Value>
New JS value
JS_STR(...)
- create a string valueJS_UTF8(...)
- same as JS_STRJS_INT(val)
- create an integer valueJS_INT32(val)
- same asJS_INT
JS_UINT32(val)
- same asJS_INT
JS_NUM(val)
- create a numeric valueJS_OFFS(val)
- same asJS_NUM
, but has a cast designed to avoidsize_t -> double
warningJS_FLOAT(val)
- same asJS_NUM
JS_DOUBLE(val)
- same asJS_NUM
JS_EXT(val)
- create an external (pointer) valueJS_BOOL(val)
- create a boolean valueJS_FUN(val)
- get a function from persistentNan::Persistent<v8::Function>
.JS_OBJ(val)
- get an object from persistentNan::Persistent<v8::Object>
.
Method check
These checks throw JS TypeError if not passed. Here T
is always used as a typename
in error messages. C
is
v8::Value
check method, like IsObject()
. I
is the index of argument as in info[I]
,
starting from 0
.
REQ_ARGS(N)
- check if at leastN
arguments passedIS_ARG_EMPTY(I)
- check if argumentI
isundefined
ornull
CHECK_REQ_ARG(I, C, T)
- check if argumentI
is approved byC
check.CHECK_LET_ARG(I, C, T)
- check if argumentI
is approved byC
check or empty.CTOR_CHECK(T)
- check if method is called as a constructorSETTER_CHECK(C, T)
- check if settervalue
is approved byC
check.DES_CHECK
- within dynamic method check if the instance wasn't destroyed bydestroy()
.
Method arguments
Two types of argument retrieval are supported: REQ_
and LET_
. The difference
is that LET_
allows the argument to be empty, using some zero-default in this case.
I
is the index of argument as in info[I]
,
starting from 0
. VAR
is the name of the variable to be created.
REQ_UTF8_ARG(I, VAR)
- requireI
'th argument to be astring
. Stored atNan::Utf8String VAR
.LET_UTF8_ARG(I, VAR)
- let optionalI
'th argument to be astring
, the default is""
. Stored atNan::Utf8String VAR
.REQ_STR_ARG(I, VAR)
- requireI
'th argument to be astring
. Stored atNan::Utf8String VAR
.LET_STR_ARG(I, VAR)
- let optionalI
'th argument to be astring
, the default is""
. Stored atNan::Utf8String VAR
.REQ_INT32_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atint VAR
.LET_INT32_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0
. Stored atint VAR
.REQ_INT32_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atint VAR
.LET_INT32_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0
. Stored atint VAR
.REQ_UINT32_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atunsigned VAR
.LET_UINT32_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0
. Stored atunsigned VAR
.REQ_BOOL_ARG(I, VAR)
- requireI
'th argument to be aboolean
. Stored atbool VAR
.LET_BOOL_ARG(I, VAR)
- let optionalI
'th argument to be aboolean
, the default isfalse
. Stored atNan::Utf8String VAR
.REQ_OFFS_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atsize_t VAR
.LET_OFFS_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0
. Stored atNan::Utf8String VAR
.REQ_DOUBLE_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atdouble VAR
.LET_DOUBLE_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0.0
. Stored atNan::Utf8String VAR
.REQ_FLOAT_ARG(I, VAR)
- requireI
'th argument to be anumber
. Stored atfloat VAR
.LET_FLOAT_ARG(I, VAR)
- let optionalI
'th argument to be anumber
, the default is0.0f
. Stored atNan::Utf8String VAR
.REQ_EXT_ARG(I, VAR)
- requireI
'th argument to be anexternal
. Stored atLocal<External> VAR
.LET_EXT_ARG(I, VAR)
- let optionalI
'th argument to be anexternal
, the default isnullptr
. Stored atNan::Utf8String VAR
.REQ_FUN_ARG(I, VAR)
- requireI
'th argument to be afunction
. Stored atLocal<Function> VAR
.REQ_OBJ_ARG(I, VAR)
- requireI
'th argument to be anobject
. Stored atLocal<Object> VAR
.REQ_ARRV_ARG(I, VAR)
- requireI
'th argument to be aTypedArray
. Stored atLocal<ArrayBufferView> VAR
.
NAN_METHOD(test) {
REQ_UINT32_ARG(0, width);
REQ_UINT32_ARG(1, height);
LET_FLOAT_ARG(2, z);
// Variables created: unsigned int width, height; float z;
...
Note: The conversion from
Nan::Utf8String
tostd::string
(viachar *
) is possible with unary*
operator.
Set properties
Set-helpers for string and numeric keys. String keys are converted to JS strings automatically.
SET_PROP(OBJ, KEY, VAL)
SET_I(ARR, I, VAL)
Set object accessors
Simplified accessor assignment, adds accessors of NAME for OBJ. Read accessor is
assumed to have the name NAME+'Getter'
and write accessor is NAME+'Setter'
.
ACCESSOR_RW(OBJ, NAME)
- add read and write accessors of NAME for OBJ.ACCESSOR_R(OBJ, NAME)
- read-only property.
void MyClass::init(Handle<Object> target) {
...
Local<ObjectTemplate> proto = ctor->PrototypeTemplate();
ACCESSOR_RW(proto, message);
...
}
NAN_GETTER(MyClass::messageGetter) { ...
NAN_SETTER(MyClass::messageSetter) { ...
Setter argument
Useful addition to NAN_SETTER macro. Works similar to method arguments. But there
is always only one required argument stored in v
.
SETTER_UTF8_ARG
- require the value to be astring
. Stored atNan::Utf8String v
.SETTER_STR_ARG
- require the value to be astring
. Stored atNan::Utf8String v
.SETTER_INT32_ARG
- require the value to be anumber
. Stored atint v
.SETTER_INT_ARG
- require the value to be anumber
. Stored atint v
.SETTER_UINT32_ARG
- require the value to be anumber
. Stored atunsigned v
.SETTER_BOOL_ARG
- require the value to be aboolean
. Stored atbool v
.SETTER_OFFS_ARG
- require the value to be anumber
. Stored atsize_t v
.SETTER_DOUBLE_ARG
- require the value to be anumber
. Stored atdouble v
.SETTER_FLOAT_ARG
- require the value to be anumber
. Stored atfloat v
.SETTER_EXT_ARG
- require the value to be anexternal
. Stored atLocal<External> v
.SETTER_FUN_ARG
- require the value to be afunction
. Stored atLocal<Function> v
.SETTER_OBJ_ARG
- require the value to be anobject
. Stored atLocal<Object> v
.SETTER_ARRV_ARG
- require the value to be aTypedArray
. Stored atLocal<ArrayBufferView> v
.
NAN_SETTER(MyClass::messageSetter) { SETTER_UTF8_ARG;
// Variable created: Nan::Utf8String v;
...
Data retrieval
-
T *getArrayData(value, num = NULL)
- extracts TypedArray data of any type from the given JS value. Does not accept Array, checked withIsArrayBufferView()
. ReturnsNULL
for empty JS values. For unacceptable values throws TypeError. -
void *getData(value)
- if value is a TypedArray, then the result ofgetArrayData(value)
is returned. Otherwise if value has'data'
property, it's content is then returned asnode::Buffer
. Returnsnullptr
in other cases.
index.js
Exports:
paths(dir)
- function. Returns a set of platform dependent paths depending on inputdir
.bin()
- prints platform binary directory absolute path.rem()
- prints a space-separated list of binary paths to be cleaned on this platform.include()
- prints include directory for thisdir
.binPath
- platform binary directory absolute path.remPath
- a space-separated list of binary paths to be cleaned on this platform.includePath
- include directory for thisdir
.
root()
- prints where'addon-tools-raub'
module is situated.include()
- prints both'addon-tools-raub'
and'nan'
include paths. Use withnode -e
through list context command expansion<!@(...)
rm()
- prints the location of'_rm.bat'
file on Windows and plainrm
on Unix.cp()
- prints the location of'_cp.bat'
file on Windows and plaincp
on Unix.mkdir()
- prints the location of'_mkdir.bat'
file on Windows and plainmkdir
on Unix.bin()
- prints platform binary directory name.binPath
- platform binary directory name.rootPath
- where'addon-tools-raub'
module is situated.includePath
- both'addon-tools-raub'
and'nan'
include paths.rmPath
- the location of'_rm.bat'
file on Windows and plainrm
on Unix.cpPath
- the location of'_cp.bat'
file on Windows and plaincp
on Unix.mkdirPath
- the location of'_mkdir.bat'
file on Windows and plainmkdir
on Unix.
Crossplatform commands
Because of the differences between Windows and Unix command shells, often a whole lot of conditions have to be introduced in binding.gyp file. Now some of them can be easily omitted with the new crossplatform commands, supplied by this package.
This comes especially handy together with GYP's executable list expansion. For example a list of files to be removed for cleaning. Or a list of unnecessary binaries to be removed upon installation of a binary-dependency package.
mkdir
On Unix, it will be an actual system mkdir
, whereas on Windows it will use the
mkdir.bat file, located at the root of this package. This BAT file behaves
as if it was a mkdir -p ...
call. You can still pass -p
switch, which is
ignored. And the limitation is that you can not create a relative-path -p
folder. This can possibly be bypassed by supplying ./-p
or something like this.
'variables': {
'mkdir' : '<!(node -e "require(\'addon-tools-raub\').mkdir()")',
},
...
'action' : ['<(mkdir)', '-p', 'binary'],
rm
Disregard del
and rd
on Windows command line. Now the same command can
be used on all platforms to remove single and multiple files and directories.
'variables': {
'rm' : '<!(node -e "require(\'addon-tools-raub\').rm()")',
'rem' : '<!(node -e "require(\'.\').rem()")',
},
...
'action' : ['<(rm)', '-rf', '<@(rem)'],
cp
For Windows the /y
flag was embedded.
'variables': {
'cp' : '<!(node -e "require(\'addon-tools-raub\').cp()")',
},
...
'action' : ['<(cp)', 'a', 'b'],
Class EventEmitter
A C++ implementation of Events API.
Note: This implementation has some minor deviations from the above standard. Specifically there is no static
EventEmitter.defaultMaxListeners
property. However the dynamic one persists and is infinite (0
) by default.
Also EventTarget is implemented. Not in full detail, but should be fine for callers.
An example can be found in examples/node-addon directory.
There is Example
class, implemented in cpp/example.cpp, that inherits
EventEmitter behavior and is exported to JS.
For the C++ side EventEmitter
has following public methods:
-
void emit(const std::string &name, int argc = 0, v8::Local<v8::Value> *argv = NULL)
- emits an event with the givenname
and, optionally, some additional arguments whereargc
is the number of arguments andargv
is a pointer to the arguments array. -
void on(const std::string &name, V8_VAR_FUNC cb)
- subscribescb
to receivename
events from this emitter, basicallyemitter.on(name, cb)
. -
void destroy()
- destroys the object, i.e. deactivates it and frees resources. This is what also called inside~EventEmitter()
, but only the first call is effective anyway.
Be sure to add the include directory in binding.gyp:
'include_dirs': [
'<!@(node -e "require(\'addon-tools-raub\').include()")',
],
Then include the event-emitter.hpp, it also includes addon-tools.hpp.
Inherit from EventEmitter
, it already inherits from Nan::ObjectWrap
:
#include <event-emitter.hpp>
class Example : public EventEmitter {
...
}
Note: Do not forget to call
EventEmitter::init()
once, in the moduleinit()
.
V8 Inheritance
Now that everything is in place, consider providing V8 with JS inheritance info:
void Example::init(Handle<Object> target) {
Local<FunctionTemplate> proto = Nan::New<FunctionTemplate>(newCtor);
// -------------------------- HERE!
// class Example extends EventEmitter
Local<FunctionTemplate> parent = Nan::New(EventEmitter::_prototype);
proto->Inherit(parent);
// --------------------------
proto->InstanceTemplate()->SetInternalFieldCount(1);
proto->SetClassName(JS_STR("Example"));
Local<Function> ctor = Nan::GetFunction(proto).ToLocalChecked();
_constructor.Reset(ctor);
Nan::Set(target, JS_STR("Example"), ctor);
}
Function consoleLog
In C++ addons, the use of iostream is discouraged because Node.js has its own
perspective on stdout behavior.
At first it may look as if cout << "msg" << endl;
works nice, but it doesn't.
After a while, it just ceases on a midword, and you end up thinking something has
broken really hard in your addon.
To overcome this, we can use some V8 eval
magic to make a real console.log
call from C++ land. And this is where consoleLog
comes into play.
-
inline void consoleLog(int argc, V8_VAR_VAL *argv)
- a generic logger, receives any set of arguments. -
inline void consoleLog(const std::string &message)
- an alias to log a single string.
Note: Don't do it in GC-accessible code: sometimes it works, sometimes it crashes.