node-haystack Episode 10: Node.js add-on
2016-09-11 16:24
513 查看
Preface
This chapter assumes you have basic concept about node.js add-on. The following sections show up a simple frame for developing node.js add-on. Before an add-on can work, there must be an object(or a set of methods). Here we just care about objects: A C++ class does the work, a node object wraps an instance of that C++ class and exports it to node system as a “js” object. So we need a wrap class to help us out.Utilities we need
We need some handy utilities. Assuming it’s under namespace jshelper:namespace jshelper { #ifndef THROW_JS_EXCEPTION /** Throw a node exception */ #define THROW_JS_EXCEPTION($iso, FMT, ...) \ ([&]{ \ LOG_ERROR(FMT, ##__VA_ARGS__); \ return ($iso)->ThrowException(js::Exception::Error(jshelper::mk_str(($iso), FMT_STR(FMT, ##__VA_ARGS__)))); \ })() #endif /** Define a node error */ #ifdef DEBUG #define JS_ERROR($iso, FMT, ...) \ ([&] { \ LOG_ERROR(FMT, ##__VA_ARGS__); \ return js::Exception::Error(jshelper::mk_str(($iso), FMT_STR(FMT, ##__VA_ARGS__))); \ })() #else #define JS_ERROR($iso, FMT, ...) js::Exception::Error(jshelper::mk_str(($iso), FMT_STR(FMT, ##__VA_ARGS__))) #endif /*! \brief Unwrap a local object. \param const js_obj_t The object to unwrap. \return T The unwrapped object. */ template<typename T> inline T* unwrap(const js_obj_t& obj) { return node::ObjectWrap::Unwrap<T>(obj); } /*! \brief Create a js string object from an utf8 string. \return The new js string object. */ inline js_str_t mk_str(js$iso_t* iso, const char* str) { return js_str::NewFromUtf8(iso, str); } /*! \brief Create a js string object from a char array, normally used as data buffer. \return The new js string object. */ inline js_str_t mk_str(js$iso_t* iso, const vec_t<char>& vec) { size_t offset = 0; // The UTF-8 BOM check comes from node_file.cc:480 if (vec.size() > 3 && 0 == std::memcpy(&vec[0], "\xEF\xBB\xBF", 3)) { offset = 3; } return js_str::NewFromUtf8(iso, &vec[offset], js_str::kNormalString, vec.size() - offset); } /*! \brief Create a js string object from an ASCII string. \return The new js string object. */ inline js_str_t mk_str(js$iso_t* iso, const std::string& str) { return js_str::NewFromUtf8(iso, str.c_str()); } /*! \brief Create an undefined object. \return The undefined object. */ inline js_val_t mk_undef(js$iso_t* iso) { return js::Undefined(iso); } /*! \brief Create a null object. \return The null object. */ inline js_val_t mk_null(js$iso_t* iso) { return js::Null(iso); } /*! \brief Create a std::string object from js value. \return The std::string object. */ inline const std::string to_string(const js_val_t& val) { js_str::Utf8Value utf8_str(val->ToString()); return std::string(*utf8_str); } /*! \brief Create a primtive bool value from js value. \return The primitive bool value. */ inline bool to_bool(const js_val_t& val) { return val->ToBoolean()->IsTrue(); } } // ns
NOTE
Don’t be fooled by modification “js”, it’s just an acronym of “v8”.
Wrap object
We need exposenode::ObjectWrap::Wrapfunction:
/*! \brief A thin wrap of node::ObjectWrap, expose #Wrap() method. */ class NodeObject : public node::ObjectWrap { public: /*! \brief v8::Handle<v8::Object> wrapper. \param v8::Handle<v8::Object> Handle to wrap. */ void Wrap (js_obj_t handle) { node::ObjectWrap::Wrap(handle); } };
Generalization
To make life easy, we had better to generalize our object wrap, making it flexible.Necessary structures
To simplify the methods and properties exporting, we need some data structures to make out code look like this:class VolumeObj { ...... IMPL_INIT_FUNC(NODE_CLS_NAME_VOL, { { "load", LoadVolume }, { "close", CloseVolume }, { "find", FindBlock }, { "addBlock", AddBlock }, { "rmBlock", RemoveBlock }, { "readData", ReadData }, { "recover", Recover }, { "verify", Verify }, }, { { "numOfBlock", BlockNum }, { "sizeOfVol", VolumeSize }, }) ... private: static void LoadVolume(const FunctionCallbackInfo<Value>& args) { ....
The declaration of data structures for methods and properties lists here:
using js_cb_t = js::FunctionCallback; using js_cb_acc_get_t = js::AccessorGetterCallback; /*! \brief Method information: the name of method and, the function. \note Used by implementation macro. */ typedef struct { std::string name; js_cb_t func; } method_info_t; /*! \brief Accessor(or property) information: the name of property and the get function. */ typedef struct { std::string name; js_cb_acc_get_t func; } accessor_info_t;
NOTE: Since we have no need to set a property, the
setfunction of properties is just ignored.
Template
A template will dramatically simplify our work of implementing a new add-on./*! \brief Template for node object. */ template <typename T> class NodeObjectTemplate : public NodeObject { public: using wrapped_t = T; using Base = NodeObjectTemplate<T>; typedef struct { js_persist_func_t cb; } callback_info_t; using cb_info_t = callback_info_t; public: explicit NodeObjectTemplate(const T* obj) { m_inner_obj = shared_t<T>(obj); } static void InitTemplate(js_obj_t exports, const char* cls_name, js_persist_func_t& ctor, const js_cb_t& new_func, const std::vector<method_info_t>& methods, const std::vector<accessor_info_t>& accessors) { js_iso_t* iso = exports->GetIsolate(); js_func_tmpl_t tpl = js_func_tmpl::New(iso, new_func); tpl->SetClassName(jshelper::mk_str(iso, cls_name)); if (methods.size() > 0) { tpl->InstanceTemplate()->SetInternalFieldCount(methods.size()); for(auto& v : methods) { NODE_SET_PROTOTYPE_METHOD(tpl, v.name.c_str(), v.func); } } js_obj_tmpl_t inst_tmpl = tpl->InstanceTemplate(); for(auto& v : accessors) { inst_tmpl->SetAccessor(jshelper::mk_str(iso, v.name), v.func); } ctor.Reset(iso, tpl->GetFunction()); exports->Set(jshelper::mk_str(iso, cls_name), tpl->GetFunction()); } static void NewInstance(const js_arg_t& args, NodeObjectTemplate<T>* obj, const js_persist_func_t& ctor) { js_iso_t* iso = args.GetIsolate(); const unsigned argc = 1; js_ext_t ext = js_ext::New(iso, obj); js_val_t argv[argc] = { ext }; js_func_t cons = js_func_t::New(iso, ctor); js_obj_t inst = cons->NewInstance(argc, argv); args.GetReturnValue().Set(inst); } template<class _T_OBJ> static void New(const js_arg_t& args, const js_persist_func_t& ctor, _T_OBJ* obj = nullptr) { js_iso_t* iso = args.GetIsolate(); if (args.IsConstructCall()) { if (args.Length() == 0) { if (obj == nullptr) { THROW_JS_EXCEPTION(iso, "CTOR_NOT_SUPPORTED"); } _T_OBJ::NewInstance(args, obj); return ; } if (!args[0]->IsExternal()) { THROW_JS_EXCEPTION(iso, "INVALID_ARGUMENTS"); return ; } LOG("Ctor call"); js_ext_t ext = js_ext_t::Cast(args[0]); _T_OBJ* _ext_obj = static_cast<_T_OBJ *>(ext->Value()); if (_ext_obj == nullptr) { THROW_JS_EXCEPTION(iso, "INVALID_OBJECT"); return ; } _ext_obj->Wrap(args.This()); args.GetReturnValue().Set(args.This()); } else { LOG("No ctor call"); js_val_t argv[1] = { args[0] }; js_func_t cons = js_func_t::New(iso, ctor); args.GetReturnValue().Set(cons->NewInstance(1, argv)); } } protected: shared_t<T> m_inner_obj; }; // template NodeObjectTemplate
note: Use
shared_ptrto make sure the inner object can be release eventually.
Macro to help exporting
We can use the following macros to export methods and properties, also implement the object creating:/** Declare the persistant constructor. */ #define DECL_CTOR() static js_persist_func_t $constructor; /** Export the constructor of specified class. */ #define EXPORT_CTOR(cls) js_persist_func_t cls::$constructor; /** Declare class's method by specified list of method's information. */ #define IMPL_INIT_FUNC(cls_name, ...) \ static void Init(js_obj_t exports) { \ InitTemplate(exports, cls_name, $constructor, New, __VA_ARGS__); \ } /** Declare the #NewInstance and #New functions. */ #define IMPL_NEW_FUNC(...) \ static void NewInstance(const js_arg_t& args, self_t* pobj) { \ Base::NewInstance(args, pobj, $constructor); \ } \ static void New(const js_arg_t& args) { \ Base::New<self_t>(args, $constructor, __VA_ARGS__); \ } /** The default implementation of creating object. */ #define DEFAULT_NEW_FUNC \ []{ \ wrapped_t* obj = new wrapped_t(); \ self_t* res = new self_t(obj); \ return res; \ }
Example
Assuming a C++ class named Random, wich provide two methods:Next, return a random 32bit integer, and
NextUuid, return a random boost::uuid value.
The code of Random class lists here:
namespace buid = boost::uuids; using uuid_t = buid::uuid; class Random { public: Random() {} inline u32 Next() { return 0x12345678; } inline uuid_t NextUuid() { return m_uuid_gen(); } inline std::string UuidToString(const uuid_t& uid) { return buid::to_string(uid); } private: buid::random_generator m_uuid_gen; };
To wrap the
Randomclass to node object, we should code like this:
#define PREPARE_FUNC(...) ... class RandomObject : public NodeObjectTemplate<Random> { public: using self_t = RandomObject; using random_t = shared_t<wrapped_t>; public: explicit RandomObject(Random* rand):Base(rand) {} IMPL_INIT_FUNC("Random", { { "next", Next }, { "nextUuid", NextUuid } }, { /* No props */ }) IMPL_NEW_FUNC(DEFAULT_NEW_FUNC()) private: ~RandomObject() { } static void Next(const js_arg_t& args) { PREPARE_FUNC(args, 0, rand) args.GetReturnValue().Set(static_cast<u32>(rand ? rand->Next() : 0)); } static void NextUuid(const js_arg_t& args) { PREPARE_FUNC(args, 0, rand) js_val_t res; if (!rand) { res = help::mk_str(iso, "00000000-0000-0000-0000-000000000000"); } else { res = help::mk_str(iso, rand->UuidToString(rand->NextUuid())); } args.GetReturnValue().Set(res); } private: DECL_CTOR() }; // cls RandomObject
The macro
PREPARE_PROPlists here:
/** Check if there are sufficient arguments. */ #define CHECK_ARGS_LEN(iso, args, num) \ if (args.Length() < num) { \ THROW_JS_EXCEPTION(iso, "INSUFFICIENT_ARGUMENT"); \ return; \ } /** Prepare environment and variables for js function implementation. */ #define PREPARE_FUNC(args, num, obj) \ js_iso_t* iso = args.GetIsolate(); \ CHECK_ARGS_LEN(iso, args, num); \ self_t* self = help::unwrap<self_t>(args.Holder()); \ if (self == nullptr) THROW_JS_EXCEPTION(iso, "INVALID_OBJECT"); \ shared_t<wrapped_t> obj = self->m_inner_obj;
Project
The most important has been done. Now we can configure the project.Some files will be listed lately.
List of project files
binding.gyppackage.json
index.js
init-mod.cxx
node-random.cxx
node-random.hpp
random.hpp
binding.gyp
{ 'targets':[ { 'target_name':'node-random', 'sources':[ 'node-random.cxx', 'init-mod.cxx' ], 'include_dirs':[ '../../include', '/usr/local/include', '/usr/include', ], 'libraries':[ '/usr/lib64/libm.so', '/usr/lib64/libpthread.so', '/usr/lib64/libboost_log.so', '/usr/lib64/libboost_thread.so', '/usr/lib64/libboost_system.so', '/usr/lib64/libboost_locale.so', '/usr/lib64/libboost_filesystem.so', ], 'cflags':[ '-DBOOST_ALL_DYN_LINK', '-DBOOST_UUID_USE_SSE2', '-fpermissive', '-fexceptions', '-std=c++17', '-fPIC', '-L/usr/local/lib/../lib64' ], 'cflags_cc!':['-fno-rtti', '-fno-exceptions'], 'cflags_cc+':['-frtti', '-fexceptions'], } ] }
package.json
{ "name": "node-random", "description": "Node random", "version": "0.0.1", "author": { "name": "igame", "url": "http://www.notexist.cn" }, "engines": { "node": ">=0.6.0" }, "main" : "index.js", "licenses": [ { "type": "free" } ], "dependencies" : { }, "scripts": { "install": "node-gyp rebuild" }, "gypfile": true, "_id": "node-random@0.0.1", "directories": {}, "readme": "ERROR: No Readme data found" }
index.js
var rand = require('./build/Debug/node-random'); module.exports = new rand.Random();
init-mod.cxx
#include "node-random.hpp" static void init(js_obj_t exports) { RandomObject::Init(exports); } NODE_MODULE(node_random, init)
node-random.cxx
#include "node-random.hpp" EXPORT_CTOR(RandomObject)
Test
Let’s write a test js. Assumingtest.jsis under directory
node-random:
var rand = require('../node-random'); console.log("Random int:", rand.next()); console.log("Random uuid:", rand.nextUuid());
NOTE: Node.js will treat the directory
node-randomas a package, that’s why the
../node-randomis important for loading module.
The output looks like:
[igame@igame-dev2 node-random]$ node test Random int: 204435980 Random uuid: addfd91a-e338-4e46-b630-b3ec2595d7f9 [igame@igame-dev2 node-random]$
NOTE: The line
Random int: 204435980does not show
0x12345678because I used my real code. It doesn’t matter the concept of this example.
Other used macros
I list other macros here, which will be used in later code./** Call a callback with varied parameters. */ #define CALL(cb, ...) { \ vec_t<js_val_t> $params({ __VA_ARGS__ }); \ cb->Call(iso->GetCurrentContext()->Global(), $params.size(), (js_val_t *)&$params[0]); \ } /** Use specified argument as the callback and call it. */ #define FIRE_CB(args, pos_of_cb_arg, ...) \ if (args.Length() > pos_of_cb_arg) \ { \ const js_val_t& $cb_val = args[pos_of_cb_arg]; \ if ($cb_val->IsFunction()) { \ js_func_t $cb = js_func_t::Cast($cb_val); \ CALL($cb, __VA_ARGS__); \ } \ } /** Call a callback with specified error message. */ #define FIRE_CB_ERR_MSG(args, pos_of_cb_arg, msg) \ FIRE_CB(args, pos_of_cb_arg, JS_ERROR(iso, msg)) /** Call a callback with error. */ #define FIRE_CB_ERR(args, pos_of_cb_arg, err) \ FIRE_CB_ERR_MSG(args, pos_of_cb_arg, ErrorToString(err)) /** Initialize a multi-thread callback.*/ #define INIT_MT_CALLBACK(args, pos_of_cb_arg) \ cb_info_t* __CALLBACK_INFO__ = nullptr; \ if (args.Length() > pos_of_cb_arg) { \ const js_val_t& $cb_val = args[pos_of_cb_arg]; \ if ($cb_val->IsFunction()) { \ __CALLBACK_INFO__ = new cb_info_t(); \ __CALLBACK_INFO__->cb.Reset(iso, js_func_t::Cast($cb_val)); \ } \ } /** Prepare js scope under another thread.*/ #define MT_SCOPE() \ js_iso_t* iso = js::Isolate::GetCurrent(); \ js::HandleScope scope(iso); /** Call the callback under another thread. */ #define MT_FIRE_CB(...) \ if (__CALLBACK_INFO__ != nullptr) { \ js_func_t $cb = js_func_t::New(iso, __CALLBACK_INFO__->cb); \ CALL($cb, __VA_ARGS__); \ __CALLBACK_INFO__->cb.Reset(); \ delete __CALLBACK_INFO__; \ } /** Call the callback with specified message, under another thread. */ #define MT_FIRE_CB_ERR_MSG(msg) \ MT_FIRE_CB({ JS_ERROR(iso, msg) }) /** Call the callback with error, under another thread. */ #define MT_FIRE_CB_ERR(err) \ MT_FIRE_CB_ERR_MSG(ErrorToString(err)) /** Prepare environment and variables for js property implementation. */ #define PREPARE_PROP(prop, obj, func) \ js_iso_t* iso = prop.GetIsolate(); \ self_t* self = help::unwrap<self_t>(prop.Holder()); \ if (self == nullptr) { \ prop.GetReturnValue().Set(func(nullptr)); \ } else { \ shared_t<wrapped_t> obj = self->m_inner_obj; \ prop.GetReturnValue().Set(func(obj)); \ }
相关文章推荐
- Step by step instructions to install NodeJS on Windows
- kissy-node run on windows解决方案,contextify nodejs windows solution
- Node.js: What is the best "full stack web framework" (with scaffolding, MVC, ORM, etc.) based on Node.js / server-side JavaScript? - Quora
- [Azure体验篇]Node.js on Azure Website
- Node.js学习(10)----文件系统fs
- Node.js + NPM + Ubuntu10
- 翻译--Blazing fast node.js: 10 performance tips from LinkedIn Mobile
- node.js global object,util and so on
- 带Checkbox的TreeView(一) 完美兼容IE、Firefox ,在js中添加了getCurrentNode(evt)方法,注册方法变为TreeView1.Attributes.Add("onclick", "CheckEvent
- NodeJS debug on eclipse
- MetaWeblog API on Nodejs
- 【转】Install Node.js and NPM on Windows
- 在LinkedIn的Ruby on Rails和Node.js对决
- How to Install and Run a node.js App on Centos 6.4 64bit
- Node.js+Express on IIS
- Blazing fast node.js: 10 performance tips from LinkedIn Mobile
- NodeJs upload files based on Express
- Node.js on Android
- How do you install Node.JS on CentOS?
- 翻译--Blazing fast node.js: 10 performance tips from LinkedIn Mobile