node-haystack Episode 10: Node.js add-on

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(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 expose node::ObjectWrap::Wrap function:

/*!
    \brief A thin wrap of node::ObjectWrap, expose #Wrap() method.
*/
class NodeObject : public node::ObjectWrap  {
public:
    /*!
         \brief v8::Handle wrapper.
         \param v8::Handle 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& 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 set function 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;

    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(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& methods, const std::vector& 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* 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 m_inner_obj;
}; // template NodeObjectTemplate

note: Use shared_ptr to 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(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 Random class to node object, we should code like this:


#define PREPARE_FUNC(...)
...

class RandomObject : public NodeObjectTemplate {
public:
    using self_t = RandomObject;
    using random_t = shared_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(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_PROP lists 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(args.Holder()); \
    if (self == nullptr) THROW_JS_EXCEPTION(iso, "INVALID_OBJECT"); \
    shared_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.gyp
  • package.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": "[email protected]",
  "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. Assuming test.js is 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-random as a package, that’s why the ../node-random is 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: 204435980 does not show 0x12345678 because 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 $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(prop.Holder()); \
    if (self == nullptr) { \
        prop.GetReturnValue().Set(func(nullptr)); \
    } else { \
        shared_t obj = self->m_inner_obj; \
        prop.GetReturnValue().Set(func(obj)); \
    }

你可能感兴趣的:(C++,JavaScript,Linux,node.js,haystack)