Search code examples
node-gypnode.js-addon

How to return a new object created in node native code via asynchronous callback?


I am trying to create a node addon that does something like this:

// js
addon.makeObject((err, obj) => {
    if (err) { /* handle error */ }
    console.log('New object ID=%d', obj.getID());
      :
});

The makeObject() is sort of an "asynchronous object factory" where a new instance is created inside the native C++ code in the background (using Nan::AsyncWorker) with necessary setups then returned back to NodeJS land via callback.

Here's a snippet of the native code:

// cpp
#include <nan.h>
#ifndef _WIN32
#include <unistd.h>
#define Sleep(x) usleep((x)*1000)
#endif

class MyClass : public Nan::ObjectWrap {
    public:
        class Worker : public Nan::AsyncWorker {
            public:
                friend class MyClass;
                explicit Worker(Nan::Callback* callback) : Nan::AsyncWorker(callback) {}

            private:
                virtual void Execute() {/* some long running task */ Sleep(1000); }
                virtual void HandleOKCallback() {
                    MyClass *mc = new MyClass();
                    v8::Local<v8::Object> obj = Nan::New<v8::Object>();
                    mc->Wrap(obj); // ==> Assertion failed: (object->InternalFieldCount() > 0)
                    v8::Local<v8::Value> argv[] = { Nan::Undefined(), obj };
                    callback->Call(2, argv);
                }

        };

        explicit MyClass() : _id(idCtr++) {}
        ~MyClass() {}

        static void Init(v8::Local<v8::Object> exports) {
            v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
            tpl->SetClassName(Nan::New("MyClass").ToLocalChecked());
            tpl->InstanceTemplate()->SetInternalFieldCount(1);
            Nan::SetPrototypeMethod(tpl, "getID", GetID);
            constructor.Reset(tpl->GetFunction());
            exports->Set(Nan::New("MyClass").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked());
        }

    private:
        int _id;

        static Nan::Persistent<v8::Function> constructor;

        static NAN_METHOD(New) {
            if (info.IsConstructCall()) {
                MyClass* mc = new MyClass();
                mc->Wrap(info.This());
                info.GetReturnValue().Set(info.This());

            } else {
                const int argc = 0;
                v8::Local<v8::Value> argv[argc] = {};
                v8::Local<v8::Function> cons = Nan::New<v8::Function>(constructor);
                info.GetReturnValue().Set(Nan::NewInstance(cons, argc, argv).ToLocalChecked());
            }
        }

        static NAN_METHOD(GetID) {
            MyClass *mc = ObjectWrap::Unwrap<MyClass>(info.Holder());
            info.GetReturnValue().Set(Nan::New<v8::Integer>(mc->_id));
        }

        static int idCtr;
};

Nan::Persistent<v8::Function> MyClass::constructor;
int MyClass::idCtr = 0;

NAN_METHOD(MakeObject) {
    // Check arguments here...
    Nan::Callback *cb = new Nan::Callback(info[0].As<v8::Function>());
    Nan::AsyncQueueWorker(new MyClass::Worker(cb)); // starts the worker
    info.GetReturnValue().Set(Nan::Undefined());
}

void InitAll(v8::Local<v8::Object> exports) {
    exports->Set(   Nan::New("makeObject").ToLocalChecked(),
                    Nan::New<v8::FunctionTemplate>(MakeObject)->GetFunction());
    MyClass::Init(exports);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

Although this compiles, it fails at this line:

                mc->Wrap(obj);

This is the error:

Assertion failed: (object->InternalFieldCount() > 0), function Wrap, file ../node_modules/nan/nan_object_wrap.h, line 55.

What is InternalFieldCount? I've been googling to find a way but with no luck so far...


Solution

  • I found an answer myself. The key was to use Nan::NewInstance on a constructor - the same way to handle JS code calling constructor as a normal function.

    Here's the complete code that works:

    // addon.cpp
    #include <nan.h>
    #ifndef _WIN32
    #include <unistd.h>
    #define Sleep(x) usleep((x)*1000)
    #endif
    
    class MyClass : public Nan::ObjectWrap {
        public:
            class Worker : public Nan::AsyncWorker {
                public:
                    friend class MyClass;
                    explicit Worker(Nan::Callback* callback) : Nan::AsyncWorker(callback) {}
    
                private:
                    virtual void Execute() {/* some long running task */ Sleep(1000); }
                    virtual void HandleOKCallback() {
                        ///////////////////////////////////////////////////////////
                        // Create MyClass instance and pass it to JS via callback
                        const int argc = 0;
                        v8::Local<v8::Value> argv[argc] = {};
                        v8::Local<v8::Function> cons = Nan::New<v8::Function>(constructor);
                        v8::Local<v8::Object> obj = Nan::NewInstance(cons, argc, argv).ToLocalChecked();
                        v8::Local<v8::Value> _argv[] = { Nan::Undefined(), obj };
                        callback->Call(2, _argv);
                    }
    
            };
    
    
            explicit MyClass() : _id(idCtr++) {}
            ~MyClass() {}
    
            static NAN_MODULE_INIT(Init) {
                v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
                tpl->SetClassName(Nan::New("MyClass").ToLocalChecked());
                tpl->InstanceTemplate()->SetInternalFieldCount(1);
                Nan::SetPrototypeMethod(tpl, "getID", GetID);
                constructor.Reset(tpl->GetFunction());
                target->Set(Nan::New("MyClass").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked());
            }
    
    
        private:
            int _id;
    
            static Nan::Persistent<v8::Function> constructor;
    
            static NAN_METHOD(New) {
                if (info.IsConstructCall()) {
                    MyClass* mc = new MyClass();
                    mc->Wrap(info.This());
                    info.GetReturnValue().Set(info.This());
    
                } else {
                    const int argc = 0;
                    v8::Local<v8::Value> argv[argc] = {};
                    v8::Local<v8::Function> cons = Nan::New<v8::Function>(constructor);
                    info.GetReturnValue().Set(Nan::NewInstance(cons, argc, argv).ToLocalChecked());
                }
            }
    
            static NAN_METHOD(GetID) {
                MyClass *mc = ObjectWrap::Unwrap<MyClass>(info.Holder());
                info.GetReturnValue().Set(Nan::New<v8::Integer>(mc->_id));
            }
    
            static int idCtr;
    };
    
    Nan::Persistent<v8::Function> MyClass::constructor;
    int MyClass::idCtr = 0;
    
    NAN_METHOD(MakeObject) {
        // Check arguments here...
        Nan::Callback *cb = new Nan::Callback(info[0].As<v8::Function>());
        Nan::AsyncQueueWorker(new MyClass::Worker(cb)); // starts the worker
        info.GetReturnValue().Set(Nan::Undefined());
    }
    
    NAN_MODULE_INIT(InitAll) {
        target->Set(    Nan::New("makeObject").ToLocalChecked(),
                        Nan::New<v8::FunctionTemplate>(MakeObject)->GetFunction());
        MyClass::Init(target);
    }
    
    NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)