I would like to access a data member of a class in JavaScript which has a user defined type.
Imagine there are two C++ classes:
class Car {
public:
SteeringWheel steeringWheel;
void start();
void stop();
}
class SteeringWheel {
public:
void turnLeft();
void turnRight();
}
I created a wrapper for both of them using N-API. How can I create an InstanceMethod or InstanceAccessor for my CarWrapper class, so I can access its data member in JavaScript:
const car = new myAddon.Car();
car.steeringWheel.turnLeft();
Your help is highly appreciated. Thank you.
I hope that the following code is helpful to you.
// C++ side
class SteeringWheel : public Napi::ObjectWrap<SteeringWheel> {
public:
SteeringWheel(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<SteeringWheel>(info) {
}
static Napi::Object Create(Napi::Env env) {
return DefineClass(env, "SteeringWheel", {
InstanceMethod("turnLeft", &SteeringWheel::TurnLeft, napi_enumerable),
InstanceMethod("turnRight", &SteeringWheel::TurnRight, napi_enumerable)
}).New({});
}
Napi::Value TurnLeft(const Napi::CallbackInfo&) {
printf("turnLeft\n");
return Napi::Value();
}
Napi::Value TurnRight(const Napi::CallbackInfo&) {
printf("turnRight\n");
return Napi::Value();
}
};
class Car : public Napi::ObjectWrap<Car> {
public:
Car(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<Car>(info), _steeringWheel(Napi::Persistent(SteeringWheel::Create(info.Env()))) {
}
Napi::Value GetSteeringWheel(const Napi::CallbackInfo&) {
return _steeringWheel.Value();
}
Napi::Value Start(const Napi::CallbackInfo& info) {
printf("start\n");
// If you need a native SteeringWheel object, then you can use Unwrap()
SteeringWheel* s = SteeringWheel::Unwrap(_steeringWheel.Value().ToObject());
s->TurnLeft(info);
s->TurnRight(info);
return Napi::Value();
}
Napi::Value Stop(const Napi::CallbackInfo&) {
printf("stop\n");
return Napi::Value();
}
static void Initialize(Napi::Env env, Napi::Object exports) {
exports.Set("Car", DefineClass(env, "Car", {
InstanceAccessor("steeringWheel", &Car::GetSteeringWheel, nullptr, napi_enumerable),
InstanceMethod("start", &Car::Start, napi_enumerable),
InstanceMethod("stop", &Car::Start, napi_enumerable)
}));
}
private:
Napi::ObjectReference _steeringWheel;
};
Napi::Object InitObjectWrap(Napi::Env env) {
Car::Initialize(env, exports);
return exports;
}
// JS side
const c = new Car();
c.steeringWheel.turnLeft();
c.start();
Hi @tastnt,
in general when you want to expose your C++ code to JavaScript, you have to extend Napi::ObjectWrap class that includes the plumbing to connect JavaScript code to a C++ object. Classes extending Napi::ObjectWrap can be instantiated from JavaScript using the new operator, and their methods can be directly invoked from JavaScript.
You can get more information about Napi::ObjectWrap from its documentation and in the on the repository dedicated to the examples there are a few that use the Napi::ObjectWrap api:
I hope that this explanation help you.
Hi both,
Thank you so much for your help!
@NickNaso Thank you. Yes, I checked all the examples and read through a big part of the docs before asking, but I have not found any example which describes the case I am outlining above, that is, having two classes, one of them holding an instance of the another as a data member; then accessing this data member in JavaScript.
@romandev Thank you for the extensive implementation. If I understand your code correctly, the SteeringWheel object gets instantiated by the wrapper class here:
Car(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<Car>(info), _steeringWheel(Napi::Persistent(SteeringWheel::Create(info.Env()))) { }
In my case my original (not wrapped) C++ Car class already creates an instance of SteeringWheel. What I would like to do is to simply return this instance.
The implementation of my Car and SteeringWheel classes are in an external DLL library.
// Car.h
class Car {
public:
SteeringWheel steeringWheel;
void start();
void stop();
}
```C++
// SteeringWheel.h
class SteeringWheel {
public:
void turnLeft();
void turnRight();
}
### Header files of the wrapper classes
```C++
// SteeringWheelWrapper.h
class SteeringWheelWrapper : public Napi::ObjectWrap<SteeringWheelWrapper> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
void turnLeft(const Napi::CallbackInfo& info);
...
}
```C++
// CarWrapper.h
class CarWrapper : public Napi::ObjectWrap
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
Napi::Value getSteeringWheel(const Napi::CallbackInfo &info);
...
private:
Car *_car;
}
### Wrapper classes
```C++
// CarWrapper.cpp
CarWrapper::CarWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<CarWrapper>(info) {
// There is a factory which takes care of the instantiation of the Car.
// Additionally, the Car itself instantiates its steeringWheel data member.
this->_car = createCar();
}
Napi::Object CarWrapper::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
Napi::Function func = DefineClass(env, "CarWrapper", {
InstanceAccessor("steeringWheel", &CarWrapper::getSteeringWheel, nullptr),
});
constructor = Napi::Persistent(func);
constructor.SuppressDestruct();
exports.Set("CarWrapper", func);
return exports;
}
Napi::Value CarWrapper::getSteeringWheel(const Napi::CallbackInfo &info) {
// Here I would have to wrap somehow the SteeringWheel instance into a SteeringWheelWrapper
// and convert it to a Napi::Value to be able to return it.
return this->_car->steeringWheel;
}
Thank you very much in advance.
Okay, I made a simple Wrap() method in SteeringWheelWrapper.
This is not perfect(creating new JS instance every time) but you might be able to modify and use it.
static Napi::Object Wrap(Napi::Env env, SteeringWheel* steeringWheel);
class SteeringWheel {
public:
void turnLeft() { printf("turnLeft\n"); }
void turnRight() { printf("turnRight\n"); }
};
class Car {
public:
SteeringWheel steeringWheel;
void start() { printf("start\n"); }
void stop() { printf("stop\n"); }
};
class SteeringWheelWrapper : public Napi::ObjectWrap<SteeringWheelWrapper> {
public:
SteeringWheelWrapper(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<SteeringWheelWrapper>(info) {
}
static Napi::Object Wrap(Napi::Env env, SteeringWheel* steeringWheel) {
Napi::HandleScope scope(env);
Napi::Object obj = DefineClass(env, "SteeringWheelWrapper", {
InstanceMethod("turnLeft", &SteeringWheelWrapper::TurnLeft, napi_enumerable),
InstanceMethod("turnRight", &SteeringWheelWrapper::TurnRight, napi_enumerable)
}).New({});
SteeringWheelWrapper* s = Unwrap(obj);
s->_steeringWheel = steeringWheel;
return obj;
}
Napi::Value TurnLeft(const Napi::CallbackInfo&) {
_steeringWheel->turnLeft();
return Napi::Value();
}
Napi::Value TurnRight(const Napi::CallbackInfo&) {
_steeringWheel->turnRight();
return Napi::Value();
}
private:
SteeringWheel* _steeringWheel;
};
class CarWrapper : public Napi::ObjectWrap<CarWrapper> {
public:
CarWrapper(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<CarWrapper>(info), _car(new Car()) {
}
Napi::Value GetSteeringWheel(const Napi::CallbackInfo& info) {
return SteeringWheelWrapper::Wrap(info.Env(), &_car->steeringWheel);
}
Napi::Value Start(const Napi::CallbackInfo&) {
_car->start();
return Napi::Value();
}
Napi::Value Stop(const Napi::CallbackInfo&) {
_car->stop();
return Napi::Value();
}
static void Initialize(Napi::Env env, Napi::Object exports) {
exports.Set("Car", DefineClass(env, "Car", {
InstanceAccessor("steeringWheel", &CarWrapper::GetSteeringWheel, nullptr, napi_enumerable),
InstanceMethod("start", &CarWrapper::Start, napi_enumerable),
InstanceMethod("stop", &CarWrapper::Start, napi_enumerable)
}));
}
private:
Car* _car;
};
This works, thank you very much!
Came across this while looking for similar issues. The code is helpful, but wondering about the lifetime of the internal Car* _car; object.
In the code, it is getting created with new as part of the constructor of CarWrapper. But it is not clear how and when it would be deleted, since there does not seem to be any equivalent delete call or destructor getting called. Since this seems to be a native C++ pointer, it does not seem like JS will take care of it either.
How to handle such scenario? How does the C++ object knows that it is safe to delete the internal pointer to the native class? Where should we implement the delete _car call?
Any help is appreciated.
@KrishnaPG you would delete the Car* in ~CarWrapper.
Thank you @gabrielschulhof That is helpful.
I added the resource cleanup in the C++ destructor. Since it involves closing files and DB connections, wanted to make sure that it is indeed working (i.e. if C++ wrapper destructor is indeed getting called and the pointer memory being freed), so added a print statement in the C++ destructor as below:
~WriterWrapper()
{
printf("\n~WriterWrapper");
delete m_pWriter;
delete m_pNGramGen;
}
Unfortunately, could not succeed in verifying. The printf never got called.
Tried calling delete on the JS side hoping it would force the c++ wrapper destruction. Did not happen.
function CallMe() {
const w = new addon.Writer("./sample.db");
delete w; // see if it forces the wrapper destruction
}
I understand GC might be involved on the JS side. But is there no practical way to unit test this kind of scenario to ensure that the C++ wrapper indeed gets destructed? How does the Node.JS / Node-addon-api team test the life-cycle events and handle such scenarios?
Never mind. Did a 10 second timeout after deleting the js variables (hoping to give some time to GC), and the destructors indeed got invoked/printed.
setTimeout(() => console.log("timeout"), 10000);
Console output:
~WriterWrappertimeout
Though, would still love to know how the Node.js/node-addon team unit tests the life-cycle / GC events, so that we could also use those techniques to ensure that we are not leaving any pointer dangling.
@KrishnaPG
node --expose-gc mytest.js
and then
{
let myObj = new addon.MyObject();
myObj = null;
global.gc();
}
We do this at https://github.com/nodejs/node-addon-api/blob/master/test/objectwrap.js#L236-L252
In our case, the constructor accepts a function which will be called from the native finalizer, so we can verify in JavaScript that the finalizer was called.
Thank you @gabrielschulhof That is helpful.
@KrishnaPG though calling global.gc() does not, officially, guarantee that myObj will be gc-ed, it has so far always worked deterministically and synchronously.
@KrishnaPG NP 馃檪
Most helpful comment
I hope that the following code is helpful to you.