Search code examples
c++node.jsnode-addon-api

Constructing native object from NodeJS Addon


Expectation

I want to to implement using a native NodeJS module that works like the javascript module below

class C1{
  make(){return new C2()}
}
class C2{}
module.exports({C1, C2})

Approach

My attempt was, (among some other failed attempts)

// so.cpp
#include <napi.h>

class C1: public Napi::ObjectWrap<C1> {
public:
  C1(const Napi::CallbackInfo &info);
  static void Initialize(Napi::Env& env, Napi::Object& target);
  Napi::Value make(const Napi::CallbackInfo &info);
};

class C2: public Napi::ObjectWrap<C2> {
  static Napi::Function constructor;
  public:
  C2(const Napi::CallbackInfo &info);
  static void Initialize(Napi::Env& env, Napi::Object& target);
  static C2 *New(const std::initializer_list<napi_value>& args);
};
Napi::Function C2::constructor;
void C2::Initialize(Napi::Env& env, Napi::Object& target){
  constructor = Napi::ObjectWrap<C2>::DefineClass(env, "C2", {});
  target.Set("C2", constructor);
}
void C1::Initialize(Napi::Env& env, Napi::Object& target) {
  Napi::Function constructor = Napi::ObjectWrap<C1>::DefineClass(env, "C1", {
    Napi::ObjectWrap<C1>::InstanceMethod("make", &C1::make),
  });
  target.Set("C1", constructor);
}

C2 *C2::New(const std::initializer_list<napi_value>& args){
  return Napi::ObjectWrap<C2>::Unwrap(constructor.New(args));
}
C2::C2(const Napi::CallbackInfo &info): Napi::ObjectWrap<C2>(info) {}
C1::C1(const Napi::CallbackInfo &info): Napi::ObjectWrap<C1>(info) {}

// make() {return new C2()}
Napi::Value C1::make(const Napi::CallbackInfo &info){
  C2 *jsclat = C2::New({});
  return jsclat->Value();
}


// module.exports = {C1, C2}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
  C1::Initialize(env, exports);
  C2::Initialize(env, exports);
  return exports;
}
NODE_API_MODULE(so, Init);

Problem

When I try to use the compiled module from javascript Node is terminated

const m = require('bindings')('hello')
const c1 = new m.c1()
c1.make() // FATAL ERROR: Error::New napi_get_last_error_info
          // Aborted

Environment

If you want the same environment as I used, here you have a dockerfile.

FROM node:13.8.0-alpine3.11
RUN apk add --upgrade git python3 g++ make
WORKDIR /opt
RUN git clone https://github.com/nodejs/node-addon-examples.git
WORKDIR /opt/node-addon-examples/1_hello_world/node-addon-api/
RUN npm install \
 && npm install --save-dev gyp
 && npx node-gyp configure 
COPY so.cpp hello.cc
RUN npx node-gyp rebuild 

Solution

  • You are not allowed to use constructor outside the function that created it - it is an automatic v8::Local reference that is allocated on the stack - you need a persistent reference to do this:

    #include <napi.h>
    
    class C1: public Napi::ObjectWrap<C1> {
    public:
      C1(const Napi::CallbackInfo &info);
      static void Initialize(Napi::Env &env, Napi::Object &target);
      Napi::Value make(const Napi::CallbackInfo &info);
    };
    
    class C2: public Napi::ObjectWrap<C2> {
      static Napi::Function constructor;
      public:
      static Napi::FunctionReference *New;
      C2(const Napi::CallbackInfo &info);
      static void Initialize(Napi::Env& env, Napi::Object& target);
    };
    
    Napi::FunctionReference *C2::New;
    
    void C2::Initialize(Napi::Env& env, Napi::Object& target){
      Napi::Function constructor = Napi::ObjectWrap<C2>::DefineClass(env, "C2", {});
      target.Set("C2", constructor);
      New = new Napi::FunctionReference();
      *New = Napi::Persistent(constructor);
    }
    void C1::Initialize(Napi::Env& env, Napi::Object& target) {
      Napi::Function constructor = Napi::ObjectWrap<C1>::DefineClass(env, "C1", {
        Napi::ObjectWrap<C1>::InstanceMethod("make", &C1::make),
      });
      target.Set("C1", constructor);
    }
    
    C2::C2(const Napi::CallbackInfo &info): Napi::ObjectWrap<C2>(info) {}
    C1::C1(const Napi::CallbackInfo &info): Napi::ObjectWrap<C1>(info) {}
    
    // make() {return new C2()}
    Napi::Value C1::make(const Napi::CallbackInfo &info){
      Napi::Value c2 = C2::New->New({});
      return c2;
    }
    
    
    // module.exports = {C1, C2}
    Napi::Object Init(Napi::Env env, Napi::Object exports)
    {
      C1::Initialize(env, exports);
      C2::Initialize(env, exports);
      return exports;
    }
    NODE_API_MODULE(so, Init);