|
||
---|---|---|
bat | ||
include | ||
test | ||
.eslintrc | ||
.gitignore | ||
.npmignore | ||
.travis.yml | ||
LICENSE | ||
README.md | ||
download.js | ||
index.js | ||
install.js | ||
package-lock.json | ||
package.json | ||
writable-buffer.js |
README.md
Addon Tools
This is a part of Node3D project.
npm i -s addon-tools-raub
Synopsis
Helpers for Node.js NAPI addons and dependency packages:
- Supported platforms (x64): Windows, Linux, OSX.
- C++ helpers:
- Macro shortcuts for NAPI.
consoleLog()
function.eventEmit()
function.getData()
function.
- Module helpers:
- Crossplatform commands for GYP:
cp
,rm
,mkdir
. - Deps unzip installer.
- Url-to-buffer downloader.
- Crossplatform commands for GYP:
Useful links: N-API Docs, Napi Docs, GYP Docs.
Jump to:
Snippets
Crossplatform commands
'variables': {
'rm' : '<!(node -p "require(\'addon-tools-raub\').rm")',
'cp' : '<!(node -p "require(\'addon-tools-raub\').cp")',
'mkdir' : '<!(node -p "require(\'addon-tools-raub\').mkdir")',
},
On both Windows and Unix those commands now have the same result:
{
'target_name' : 'make_directory',
'type' : 'none',
'dependencies' : ['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/addon.node',
'<(binary)/addon.node'
],
}],
},
Addon binary directory
'variables': {
'binary' : '<!(node -p "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 may help within the following guidelines:
-
In package.json use a
"postinstall"
script to download the libraries. For example the following structure might work. Note that Addon Tools will append any given URL with/${platform}.zip
"config" : { "install" : "v1.0.0" }, "scripts": { "postinstall": "install", },
where
config.install
is YOUR install.js is:const install = require('addon-tools-raub/install'); const prefix = 'https://github.com/user/addon/releases/download'; const tag = process.env.npm_package_config_install; install(`${prefix}/${tag}`);
Addon Tools will unzip the downloaded file into the platform binary directory. E.g. on Windows it will be bin-windows.
-
Per platform binary directories:
- 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);
-
Publishing is done by attaching a zipped platform folder to the Github release. Zip file must NOT contain platform folder as a subfolder, but rather contain the final binaries. The tag of the release should be the same as in
npm_package_config_install
- that is the way installer will find it. -
NOTE: You can publish your binaries to anywhere, not necessarily Github. Just tweak the install.js script as appropriate. The only limitation from Addon Tools is that it should be a zipped set of files/folders.
Compiled addon
With the advent of N-API the focus of compiled addons shifted towards the word "addons". Since ABI is now compatible across Node.js versions, now addons are just plain DLLs. Therefore distribution of the binaries is covered in the previous section. But for an addon you have to provide a GYP compilation step. N-API changes it's role to out-of-install compilation, so we can't/shouldnt put the file binding.gyp to the module root anymore.
The workaround would be to have a separate directory within your project (with simplified package.json) for the sake of addon compilation.
{
"name": "build",
"version": "0.0.0",
"private": true,
"dependencies": {
"addon-tools-raub": "5.0.0",
"deps-EXT_LIB": "1.0.0"
}
}
- 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
subdirectory.
That together with binding.gyp, this would be enough to get the addon compiled. Then the binaries are published to the Github release. When the addon is installed, its index.js is responsible for reexport of the binary. Just require the built module like this:
const { bin } = require('addon-tools-raub');
const core = require(`./${bin}/MY_ADDON`);
See a snipped for binding.gyp here
- 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
subdirectory.
{
'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',
],
'xcode_settings': {
'DYLIB_INSTALL_NAME_BASE': '@rpath',
},
}
],
[
'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',
],
}],
},
]
}
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>
In gyp, the include directory should be set for your addon to know where to get it. An actual path to the directory is exported from the module and is accessible like this:
require('addon-tools-raub').include // a string
Helpers in addon-tools.hpp:
Usually all the helpers work within the context of JS call. In this case we
have CallbackInfo info
passed as an argument.
#define NAPI_ENV Napi::Env env = info.Env();
#define NAPI_HS Napi::HandleScope scope(env);
Return value
RET_VALUE(VAL)
- return a given Napi::Value.RET_UNDEFINED
- returnundefined
.RET_NULL
- returnnull
.RET_STR(VAL)
- returnNapi::String
, expectedVAL
isconst char *
.RET_NUM(VAL)
- returnNapi::Number
, expectedVAL
isdouble
.RET_EXT(VAL)
- returnNapi::External
, expectedVAL
isvoid *
.RET_BOOL(VAL)
- returnNapi::Boolean
, expectedVAL
isbool
.RET_FUN(VAL)
- returnNapi::Function
, expectedVAL
is anapi_value
.RET_OBJ(VAL)
- returnNapi::Object
, expectedVAL
is anapi_value
.
New JS value
JS_STR(VAL)
- create aNapi::String
value.JS_NUM(VAL)
- create aNapi::Number
value.JS_EXT(VAL)
- create aNapi::External
(from pointer) value.JS_BOOL(VAL)
- create aNapi::Boolean
value.JS_FUN(VAL)
- create aNapi::Function
value.JS_OBJ(VAL)
- create aNapi::Object
value.
Method check
These checks throw JS TypeError if not passed. Here T
is always used as a typename
in error messages. C
is a
Napi::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
The idea is to ease the transition from what inside the CallbackInfo
to
what you work with in C++.
Three types of argument retrieval are supported: REQ_
, USE_
and LET_
.
The difference:
REQ_
- 2 params, requires an argument to have a valueUSE_
- 3 params, allows the argument to be empty and have a defaultLET_
- 2 params, isUSE_
with a preset zero-default.
What it does, basically:
// REQ_DOUBLE_ARG(0, x)
double x = info[0].ToNumber().DoubleValue();
// USE_DOUBLE_ARG(0, x, 5.7)
double x = IS_ARG_EMPTY(0) ? 5.7 : info[0].ToNumber().DoubleValue();
// LET_DOUBLE_ARG(0, x)
double x = IS_ARG_EMPTY(0) ? 0.0 : info[0].ToNumber().DoubleValue();
That extrapolates well to all the helpers below:
REQ_STR_ARG
- JSstring
=> C++std::string
.USE_STR_ARG
LET_STR_ARG
- default:""
.REQ_INT32_ARG
- JSnumber
=> C++int32_t
.USE_INT32_ARG
LET_INT32_ARG
- default:0
.REQ_INT_ARG
- JSnumber
=> C++int32_t
.USE_INT_ARG
LET_INT_ARG
- default:0
.REQ_UINT32_ARG
- JSnumber
=> C++uint32_t
.USE_UINT32_ARG
LET_UINT32_ARG
- default:0
.REQ_UINT_ARG
- JSnumber
=> C++uint32_t
.USE_UINT_ARG
LET_UINT_ARG
- default:0
.REQ_BOOL_ARG
- JSBoolean
=> C++bool
.USE_BOOL_ARG
LET_BOOL_ARG
- default:false
.REQ_OFFS_ARG
- JSnumber
=> C++size_t
.USE_OFFS_ARG
LET_OFFS_ARG
- default:0
.REQ_DOUBLE_ARG
- JSnumber
=> C++double
.USE_DOUBLE_ARG
LET_DOUBLE_ARG
- default:0.0
.REQ_FLOAT_ARG
- JSnumber
=> C++float
.USE_FLOAT_ARG
LET_FLOAT_ARG
- default:0.f
.REQ_EXT_ARG
- JSnative
=> C++void*
.USE_EXT_ARG
LET_EXT_ARG
- default:nullptr
.REQ_FUN_ARG
- JSfunction
=> C++Napi::Function
.REQ_OBJ_ARG
- JSobject
=> C++Napi::Object
.USE_OBJ_ARG
LET_OBJ_ARG
- default:{}
.REQ_ARRV_ARG
- JSArrayBuffer
=> C++Napi::ArrayBuffer
.REQ_BUF_ARG
- JSBuffer
=> C++Napi::Buffer<uint8_t>
.
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;
...
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(CLASS, NAME)
- add read and write accessors NAME of CLASS.ACCESSOR_R(CLASS, NAME)
- add read accessor NAME of CLASS.ACCESSOR_M(CLASS, NAME)
- add method NAME of CLASS.
void MyClass::init(Napi::Env env, Napi::Object exports) {
...
Napi::Function ctor = DefineClass(env, "MyClass", {
ACCESSOR_R(MyClass, isDestroyed),
ACCESSOR_RW(MyClass, x),
ACCESSOR_M(MyClass, reset),
});
...
}
JS_GETTER(MyClass::isDestroyedGetter) { ...
JS_GETTER(MyClass::xGetter) { ...
JS_SETTER(MyClass::xSetter) { ...
JS_METHOD(MyClass::save) { ...
Setter argument
Works similar to method arguments. But there is always value
argument, from which a C++ value is extracted.
SETTER_STR_ARG
SETTER_INT32_ARG
SETTER_INT_ARG
SETTER_BOOL_ARG
SETTER_UINT32_ARG
SETTER_UINT_ARG
SETTER_OFFS_ARG
SETTER_DOUBLE_ARG
SETTER_FLOAT_ARG
SETTER_EXT_ARG
SETTER_FUN_ARG
SETTER_OBJ_ARG
SETTER_ARRV_ARG
JS_SETTER(MyClass::x) { SETTER_STR_ARG;
// Variable created: std::string v;
...
Data retrieval
-
T *getArrayData(value, num = NULL)
- extracts TypedArray data of any type from the given JS value. Does not acceptArray
. Checks withIsArrayBuffer()
. Returnsnullptr
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
- platform binary directory absolute path.include
- include directory for thisdir
.
include
- both'addon-tools-raub'
and'node-addon-api'
include paths. Use withnode -p
through list context command expansion<!@(...)
rm
- the location of'_rm.bat'
file on Windows and plainrm
on Unix.cp
- the location of'_cp.bat'
file on Windows and plaincp
on Unix.mkdir
- the location of'_mkdir.bat'
file on Windows and plainmkdir
on Unix.bin
- platform binary directory name.
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 -p "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")',
},
...
'action' : ['<(rm)', '-rf', 'dirname'],
cp
For Windows the /y
flag was embedded.
'variables': {
'cp' : '<!(node -e "require(\'addon-tools-raub\').cp")',
},
...
'action' : ['<(cp)', 'a', 'b'],
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 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: only use this within JS function stack
Function eventEmit
In N-API there is no inheritance. And no eval('require(...)')
possible at
the time of module init. But require('events')
is very tempting...
The solution is:
// JS
const EventEmitter = require('events');
const { bin } = require('addon-tools-raub');
const MyClass = require(`./${bin}/addon`);
MyClass.prototype.__proto__ = EventEmitter.prototype;
// Since now it is possible to call `emit` from instancess of MyClass
// C++
void MyClass::emit(const Napi::CallbackInfo& info, const char* name) {
NAPI_ENV;
THIS_OBJ(that);
eventEmit(env, that, name);
}
Signature:
inline void eventEmit(
Napi::Env env,
Napi::Object that,
const std::string &name,
int argc = 0,
Napi::Value *argv = nullptr
)