Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I return a native object from a class derived from Nan::ObjectWrap?

I have two simple classes A and B that I'm trying to expose in a native module in node.js. A is directly creatable, but a B is only created by calling A::foo().

class Internal {};

class B {
  public:
    Internal internal;
    explicit B(Internal internal):internal(internal){}
};

class A {
  public:
    A() : internal() {};
    B foo() { return B(internal); }
  private:
    Internal internal;
};

I want to be able to write:

const M = require('node_nan_minimal');
const a = new M.A();
const b = a.foo();

To do this I'm creating two wrapper classes deriving from Nan::ObjectWrap

class AWrapper : public Nan::ObjectWrap { ... }
class BWrapper : public Nan::ObjectWrap { ... }

Each contains an instance of A or B respectively. With these I can create an object of type A from within javascript, but I'm having trouble with the implementation of AWrapper::foo.

static NAN_METHOD(foo) {
  AWrapper* obj = Nan::ObjectWrap::Unwrap<AWrapper>(info.Holder());
  B b = obj->a_.foo();
  BWrapper * result = new BWrapper(b);
  // Something to get a B object to javascript
  // ...
  // info.GetReturnValue().Set(result->Wrap());
  // ...
  // doesn't work - so what should it be?
}

What do I do to make this function work?


The complete code for the .cc file is

#include <node.h>
#include <nan.h>

class Internal {
};

class B {
  public:
    Internal internal;
    explicit B(Internal internal):internal(internal){}
};

class A {
  public:
    A() : internal() {};
    B foo() { return B(internal); }
  private:
    Internal internal;
};

class BWrapper : public Nan::ObjectWrap {
  public:
    B b_;
    explicit BWrapper(B b) : b_(b) {}
    ~BWrapper() {}
};


class AWrapper : public Nan::ObjectWrap {
  public:
  A a_;
  explicit AWrapper(A a) : a_(a) {}
  ~AWrapper() {}

  static void register_class(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
    v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
    tpl->SetClassName(Nan::New("A").ToLocalChecked());
    tpl->InstanceTemplate()->SetInternalFieldCount(1);

    Nan::SetPrototypeMethod(tpl, "foo", foo);

    constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked());
    Nan::Set(target, Nan::New("A").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked());
  }

 private:

  static NAN_METHOD(New) {
    if (info.IsConstructCall()) {
      A a;
      AWrapper *obj = new AWrapper(a);
      obj->Wrap(info.This());
      info.GetReturnValue().Set(info.This());
    } else {
      const int argc = 1;
      v8::Local<v8::Value> argv[argc] = {info[0]};
      v8::Local<v8::Function> cons = Nan::New(constructor());
      info.GetReturnValue().Set(cons->NewInstance(argc, argv));
    }
  }

  static NAN_METHOD(foo) {
    AWrapper* obj = Nan::ObjectWrap::Unwrap<AWrapper>(info.Holder());
    B b = obj->a_.foo();
    BWrapper * result = new BWrapper(b);
    // Something to get a B object to javascript
    //...
    //info.GetReturnValue().Set(result->Wrap());
  }

  static inline Nan::Persistent<v8::Function> & constructor() {
    static Nan::Persistent<v8::Function> my_constructor;
    return my_constructor;
  }

};


NAN_MODULE_INIT(InitModule) {
  AWrapper::register_class(target);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitModule);

And a complete repository of the example can be found at https://github.com/mikeando/node_nan_minimal which you should be able to clone and then build using npm install.

like image 686
Michael Anderson Avatar asked Jan 17 '18 13:01

Michael Anderson


1 Answers

One way to make this work is:

  • add a constructor to BWrapper, but dont expose that function to javascript. Make this function take a pointer to a B. This will need to be stored in an init_class function.
  • add a function to BWrapper to create a new instance using this constructor. This will also take a pointer to a B.
  • make the foo function call the BWrapper::NewInstance.

This amounts to the following additions

class BWrapper {

  ...

  static void init_class() {
    v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
    tpl->SetClassName(Nan::New("B").ToLocalChecked());
    tpl->InstanceTemplate()->SetInternalFieldCount(1);
    constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked());
  }

  static NAN_METHOD(New) {
         if (!info.IsConstructCall()) {
      return Nan::ThrowError("File() must be called as a constructor");
    }

    if (info.Length() != 1 || ! info[0]->IsExternal()) {
        return Nan::ThrowError("File() can only be called internally");
    }

    B* b = static_cast<B*>(info[0].As<v8::External>()->Value());
    BWrapper *obj = new BWrapper(*b);
    obj->Wrap(info.This());
    info.GetReturnValue().Set(info.This());
  }

  static v8::Local<v8::Object> NewInstance(B* b) {
    Nan::EscapableHandleScope scope;

    const unsigned argc = 1;
    v8::Local<v8::Value> argv[argc] = { Nan::New<v8::External>(b) };
    v8::Local<v8::Function> cons = Nan::New<v8::Function>(constructor());
    v8::Local<v8::Object> instance = cons->NewInstance(argc, argv);

    return scope.Escape(instance);
  }

  static inline Nan::Persistent<v8::Function> & constructor() {
    static Nan::Persistent<v8::Function> my_constructor;
    return my_constructor;
  }
}

And the changes to AWrapper::foo are:

   static NAN_METHOD(foo) {
     AWrapper* obj = Nan::ObjectWrap::Unwrap<AWrapper>(info.Holder());
     B b = obj->a_.foo();
     info.GetReturnValue().Set(BWrapper::NewInstance(&b));
   }

And finally we need to ensure the class gets registered.

 NAN_MODULE_INIT(InitModule) {
   BWrapper::init_class();
   AWrapper::register_class(target);
 }

I suspect this is not the cleanest way to do it, and I'd love to see any alternatives. I'm particularly interested in whether exposing the constructor to BWrapper in this way has any downsides.

Partly this was taken from reading https://github.com/tracelytics/node-traceview-bindings/blob/master/src/metadata.cc#L18 at the suggestion of another question and then a dash of my own experimentation.

like image 115
Michael Anderson Avatar answered Oct 24 '22 10:10

Michael Anderson