Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ObjectWrap Factory? #744

Closed
bsrdjan opened this issue Jun 18, 2020 · 12 comments
Closed

ObjectWrap Factory? #744

bsrdjan opened this issue Jun 18, 2020 · 12 comments

Comments

@bsrdjan
Copy link

bsrdjan commented Jun 18, 2020

I could not get the ObjectWrap Factory example myobject.cc running, because env.SetInstanceData() and env.GetInstanceData() methods do not exist. Is there a working example?

Trying to create the Child ObjectWrap object instance, I call the Child::Init method. The instance is created but the Child::Child constructor not called and the instance not properly initialised. How to create an initialised Child object instance?

@bsrdjan
Copy link
Author

bsrdjan commented Jun 22, 2020

I could not find the N-API version of this example: https://nodejs.org/api/addons.html#addons_factory_of_wrapped_objects

Is it available or could someone pls share similar N-API example?

@KevinEady
Copy link
Contributor

Hi @bsrdjan ,

Thank you for your patience. I must have overlooked your first question.

Firstly, I'm not sure why you are having problems with the myobject.cc example. Napi::Env::SetInstanceData and GetInstanceData were added in N-API 5 and introduced in node-addon-api #663 and is included in the currently-released node-addon-api@3.0.0. The example's package.json also requires an N-API 5 compatible node engine, so you shouldn't be able to install on any system where node does not ship with N-API 5+ support.

I just did this on node-addon-examples master branch:

$ cd 7_factory_wrap/node-addon-api

$ node -v
v12.18.0

$ npm i

> factory_wrap@0.0.0 install /Users/kevineady/Documents/Projects/node-addon-examples/7_factory_wrap/node-addon-api
> node-gyp rebuild

  CXX(target) Release/obj.target/addon/addon.o
  CXX(target) Release/obj.target/addon/myobject.o
  SOLINK_MODULE(target) Release/addon.node
npm notice created a lockfile as package-lock.json. You should commit this file.
added 2 packages from 58 contributors and audited 2 packages in 9.273s
found 0 vulnerabilities

$ node . 
11
12
13
21
22
23

Regarding a node-addon-api example of factory of wrapped objects... Let me get back to you on that 😄

@KevinEady
Copy link
Contributor

After looking myobject example, this does exemplify the "factory of wrapped objects" paradigm:

The alternative would be, for example, replacing line 8 with:

var obj2 = new createObject.MyObject(20);

[NB: MyObject is exposed through the exports by the "child" initialization call of MyObject::Init]

Does this help / answer your question?

Let us know if you have any additional questions!

Thanks, Kevin

@bsrdjan
Copy link
Author

bsrdjan commented Jun 24, 2020

Thank you very much Kevin, the problem was my N-API version. It was updated only in package.json but not in CMakeLists.txt. After CMake fixed, Set/GetInstance are found and all examples run fine.

@bsrdjan bsrdjan closed this as completed Jun 24, 2020
@bsrdjan bsrdjan reopened this Jun 24, 2020
@bsrdjan
Copy link
Author

bsrdjan commented Jun 24, 2020

The given example works for my use-case and there are two details I am not sure about. In extended example, when SetInstance data called for both classes, the "TypeError: obj.plusOne is not a function" is raised if MyFactory::Init
comes after MyObject::Init in Register module. Otherwise works fine, or with workaround applied in MyFactory::Init. Why is that and how the SetInstanceData could be called separately, for each object?

myobject.h

#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <napi.h>

class MyObject : public Napi::ObjectWrap<MyObject>
{
public:
  static Napi::Object Init(Napi::Env env, Napi::Object exports);
  static Napi::Object NewInstance(Napi::Env env, Napi::Value arg);
  MyObject(const Napi::CallbackInfo &info);

private:
  Napi::Value PlusOne(const Napi::CallbackInfo &info);
  double counter_;
};

class MyFactory : public Napi::ObjectWrap<MyFactory>
{
public:
  static Napi::Object Init(Napi::Env env, Napi::Object exports);
  MyFactory(const Napi::CallbackInfo &info);

  static Napi::FunctionReference constructor;

private:
  Napi::Value CreateObject(const Napi::CallbackInfo &info);
};

#endif

myobject.cc

#include "myobject.h"
#include <napi.h>
#include <uv.h>

using namespace Napi;

Napi::Object
MyObject::Init(Napi::Env env, Napi::Object exports)
{
  Napi::HandleScope scope(env);

  Napi::Function func = DefineClass(
      env, "MyObject", {
                           InstanceMethod("plusOne", &MyObject::PlusOne),
                       });

  Napi::FunctionReference *constructor = new Napi::FunctionReference();
  *constructor = Napi::Persistent(func);
  env.SetInstanceData(constructor);

  exports.Set("MyObject", func);
  return exports;
}

MyObject::MyObject(const Napi::CallbackInfo &info)
    : Napi::ObjectWrap<MyObject>(info)
{
  Napi::Env env = info.Env();
  Napi::HandleScope scope(env);

  this->counter_ = info[0].As<Napi::Number>().DoubleValue();
};

Napi::Object MyObject::NewInstance(Napi::Env env, Napi::Value arg)
{
  Napi::EscapableHandleScope scope(env);
  Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New({arg});
  return scope.Escape(napi_value(obj)).ToObject();
}

Napi::Value MyObject::PlusOne(const Napi::CallbackInfo &info)
{
  Napi::Env env = info.Env();
  this->counter_ = this->counter_ + 1;

  return Napi::Number::New(env, this->counter_);
}

addon.cc

#include <napi.h>
#include "myobject.h"

Napi::FunctionReference MyFactory::constructor;

Napi::Object MyFactory::Init(Napi::Env env, Napi::Object exports)
{
  Napi::HandleScope scope(env);

  Napi::Function func = DefineClass(env,
                                    "MyFactory", {
                                                     InstanceMethod("createObject", &MyFactory::CreateObject),
                                                 });

  // Error "TypeError: obj.plusOne is not a function" if MyFactory::Init
  // comes after MyObject::Init in Register module and SetInstanceData
  // called for MyFactory as well:
  //
  // Napi::FunctionReference *constructor = new Napi::FunctionReference();
  // *constructor = Napi::Persistent(func);
  // env.SetInstanceData(constructor);
  // constructor->SuppressDestruct();
  //
  // Workaround:
  constructor = Napi::Persistent(func);
  constructor.SuppressDestruct();

  exports.Set("MyFactory", func);
  return exports;
}

Napi::Value MyFactory::CreateObject(const Napi::CallbackInfo &info)
{
  return MyObject::NewInstance(info.Env(), info[0]);
}

MyFactory::MyFactory(const Napi::CallbackInfo &info)
    : Napi::ObjectWrap<MyFactory>(info)
{
  Napi::Env env = info.Env();
  Napi::HandleScope scope(env);
};

Napi::Object RegisterModule(Napi::Env env, Napi::Object exports)
{
  MyObject::Init(env, exports);
  MyFactory::Init(env, exports);
  return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, RegisterModule)

addon.js

var addon = require("bindings")("addon");

var factory = new addon.MyFactory();

var obj = factory.createObject(10);
console.log(obj.plusOne()); // 11
console.log(obj.plusOne()); // 12
console.log(obj.plusOne()); // 13

var obj2 = factory.createObject(20);
console.log(obj2.plusOne()); // 21
console.log(obj2.plusOne()); // 22
console.log(obj2.plusOne()); // 23

@bsrdjan
Copy link
Author

bsrdjan commented Jun 24, 2020

The 2nd question is about following scenario. The MyFactory instance should be created with the object, to be used as initial value for factory created objects. In example below, the Napi::Object {k1: 10} should be saved in MyFactory instance and the new created MyObjects should count from 10:

var addon = require("bindings")("addon");

var factory = new addon.MyFactory({ k1: 10 });

var obj = factory.createObject();
console.log(obj.plusOne()); // 11
console.log(obj.plusOne()); // 12
console.log(obj.plusOne()); // 13

var factory2 = new addon.MyFactory({ k1: 20 });
var obj2 = factory2.createObject();
console.log(obj2.plusOne()); // 21
console.log(obj2.plusOne()); // 22
console.log(obj2.plusOne()); // 23

How to pass this "initializer" object to MyObject constructor?

Napi::Object MyObject::NewInstance(const Napi::CallbackInfo &info, Napi::Value arg)
{
  Napi::Env env = info.Env();
  Napi::EscapableHandleScope scope(env);
  Napi::Object objInit = arg.As<Napi::Object>();
  double counter = objInit.Get(Napi::String::New(env, "k1")).As<Napi::Number>().DoubleValue();
  // how to call the MyObject constructor here?
  //Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New({arg});
  //return scope.Escape(napi_value(obj)).ToObject();
}

@KevinEady
Copy link
Contributor

Hi @bsrdjan ,

When using SetInstanceData, any subsequent call will overwrite any previous calls' SetInstanceData. It looks like you want to call SetInstanceData multiple times, and that order of which-gets-called-first is what's causing the erratic behavior.

If you want to use SetInstanceData multiple times, perhaps use a struct that has members for every "piece of data" you want to store...? Then, every time you want to "update instance data", you'd need to GetInstanceData() and modify the member? Just a thought.

Thanks, Kevin

@bsrdjan
Copy link
Author

bsrdjan commented Jun 30, 2020

Thanks, it solved my issue.

@bsrdjan bsrdjan closed this as completed Jun 30, 2020
@sigmasoldi3r
Copy link

Sorry for reviving this old issue, but, is there any example out there for multiple factory methods?

The current example (As mentioned above) will only work for one.

I have to setup a whole new object, and use it each time? Or is there any cleaner method?

Thanks!

@dennislogghe-tomtom
Copy link

Hello @KevinEady,
Would it be possible to provide an example for your suggestion on how to use SetInstanceData will multiple different persistent class constructors (FunctionReference instances)? I've tried it out but without success.

@KevinEady
Copy link
Contributor

Hi @dennislogghe-tomtom ,

If you store an ObjectReference as your instance data (instead of the FunctionReference as in the factory_wrap example and the code above), you will be able to store the constructor Functions as key-value pairs in this object.

template <typename K>
inline Napi::Function GetInstanceData(Napi::Env env, K key) {
  auto data = env.GetInstanceData<Napi::ObjectReference>();

  if ( !data ) {
    return Napi::Function();
  }

  return data->Get(key).template As<Napi::Function>();
}

template <typename K>
inline void AttachInstanceData(Napi::Env env, K key, Napi::Function constructor) {
  auto data = env.GetInstanceData<Napi::ObjectReference>();

  if ( !data ) {
    data = new Napi::ObjectReference();
    *data = Napi::Reference<Napi::Object>::New(Napi::Object::New(env), 1);
    env.SetInstanceData(data);
  }

  data->Set(key, constructor);
}

Then, you can use it like so (in the above example):

Napi::Object MyObject::Init(Napi::Env env, Napi::Object exports) {
  Napi::Function func = DefineClass(
      env, "MyObject", {InstanceMethod("plusOne", &MyObject::PlusOne)});

  AttachInstanceData(env, "MyObject", func);

  exports.Set("MyObject", func);
  return exports;
}

Napi::Object MyFactory::Init(Napi::Env env, Napi::Object exports)
{
  Napi::HandleScope scope(env);

  Napi::Function func = DefineClass(env,
                                    "MyFactory", {
                                                     InstanceMethod("createObject", &MyFactory::CreateObject),
                                                 });
                                                 
  AttachInstanceData(env, "MyFactory", func);
  exports.Set("MyFactory", func);
  return exports;
}
Napi::Object MyObject::NewInstance(Napi::Env env, Napi::Value arg) {
  Napi::EscapableHandleScope scope(env);
  Napi::Object obj = GetInstanceData(env, "MyObject").New({arg});
  return scope.Escape(napi_value(obj)).ToObject();
}

cc: @sigmasoldi3r / @bsrdjan

Let me know if this helps!

Thanks, Kevin

@dennislogghe-tomtom
Copy link

@KevinEady works like a charm, thanks a lot for your quick response and the time you took to write up this clear code sample!

I came across a comment that is referring to the same problem (added by @djulien in nodejs/node-addon-examples@4c3b781). Maybe it would be good to create a new example or extend an existing one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants