C++ Class Inheritance with Node-API (and node-addon-api)

Momtchil Momtchev
7 min readJan 14, 2023

Including deriving a C++ class from EventEmitter

Photo by Alina Grubnyak on Unsplash

All the example code can be found in this repository:

During the last few years most major Node.js addons switched from the older NAN (Native Abstractions for Node.js) to the newer Node-API. It offers numerous advantages: fully compatible binary ABI across different Node.js versions, native C++ class and object semantics — including constructors and destructors — and full compatibility between C++ and JS exceptions. During those years, only one major problem has remained unsolved and that is the question of C++ class inheritance — which used to work very well in NAN — and it is still not supported in Node-API. For example, the largest addon that I maintain — the GDAL bindings for Node.js — makes extensive use of C++ class inheritance — and until recently I have been unable to come up with a satisfying solution.

I will very briefly sum up the reasons why this problem has remained stalled for the last 5 years since it was first discussed in an issue:

I won’t go through all the details of why this can’t be done in Node-API — you can read the rest — but it all comes down to V8 being unable to retrieve the v8::FunctionTemplate used for creating a v8::Function. As Node-API aims to be an universal and future-proof API for interfacing with any JS engine — even if V8 is still the only one for which there is full support, its project team refuse to extend it to provide access to a V8-specific feature. NAN, which was tied to the V8 engine did not have this problem as it simply allowed access to v8::FunctionTemplate::Inherit . However, even with NAN, extending from EventEmitter was not possible — as there was no way to retrieve its v8::FunctionTemplate .

There are various approaches to solving the problem — none of them are perfect — and you can read about some of them in the issue. Throughout this story, I will show you mine which I think is the least bad.

The requirement

  • Use only Node-API, no direct V8 calls which will result in loss of binary compatibility
  • In JS, the classes must appear inherited for the end-user when using instanceof
  • The C++ implementation can share method code in a base class
  • Support inheriting from EventEmitter (ie inheriting from a JS class)

Case 1: Inheriting a C++ class from another C++ class and exporting the class hierarchy to JavaScript

As you know, in Node-API all C++ classes that will be exported to JS must inherit from ObjectWrap<T> which is a CRTP class.

If we want to avoid the dreaded Illegal invocation V8 error, we will have to have different template parameters in the superclass and in the subclass. There is only way to make it and this is to declare another CRTP class:

template <class T> class Base: public Napi::ObjectWrap<T> {
public:
Base(const Napi::CallbackInfo &);
virtual ~Base(){};

static Napi::Function GetClass(Napi::Env);

// shared methods go here
virtual Napi::Value Do(const Napi::CallbackInfo &);
Napi::Value Tell(const Napi::CallbackInfo &);
};

This will be our Base class. It will hold all the members that will be shared between the subclasses. Now, the problem is that this class cannot be instantiated, so will have to create a special leaf class that we will be using when instantiating Base :

class BaseInstance: public Base<BaseInstance> {
public:
using Base::Base;
virtual ~BaseInstance(){};
};

We can still export this class to JavaScript under the name Base if we desire so. When we want to extend Base we will be using Base , and when we want to instantiate it — we will be using BaseInstance . Exporting the class to JS is straight-forward:

template <class T> Napi::Function Base<T>::GetClass(Napi::Env env) {
return DefineClass(env, "Base",
{
InstanceMethod("do", &Base::Do),
InstanceMethod("tell", &Base::Tell)
});
}

And then when registering the module we can simply do:

exports.Set("Base", BaseInstance::GetClass(env));

Note that we name it Base but we actually call BaseInstance .

Let’s now extend this class. Create another leaf class that will hold the extended methods, nothing too fancy here:

class Extended : public Base<Extended> {
public:
Extended(const Napi::CallbackInfo &);
virtual ~Extended(){};

static Napi::Function GetClass(Napi::Env);

// This method overrides the one in the base class
virtual Napi::Value Do(const Napi::CallbackInfo &);
};

The secret of this recipe is the way we register the subclass in V8:

Napi::Function Extended::GetClass(Napi::Env env) {
return ObjectWrap<Extended>::DefineClass(
env, "Extended",
{
// Override this method
ObjectWrap<Extended>::InstanceMethod("do", &Extended::Do),
// Inherit this method
ObjectWrap<Extended>::InstanceMethod("tell", &Base::Tell),
});
}

The C++ inheritance is now set up. We need just one last step to make instanceof in JavaScript work properly and this is accomplished by using Object.setPrototypeOf :

Napi::Function ClassBase = BaseInstance::GetClass(env);
Napi::Function ClassExtended = Extended::GetClass(env);
exports.Set("Base", ClassBase);
exports.Set("Extended", ClassExtended);
Napi::Function setProto = env.Global()
.Get("Object")
.ToObject()
.Get("setPrototypeOf")
.As<Napi::Function>();
setProto.Call({ClassExtended, ClassBase});
setProto.Call({ClassExtended.Get("prototype"),
ClassBase.Get("prototype")});

In this example, as through-out the rest of the tutorial I won’t be making any error-checking — it is highly recommended that you enable C++ exceptions if you intend to do this too.

We retrieve the V8 method Object.setPrototypeOf and then we setup the JS prototype chain to make the two classes appear inherited. Voila, we are done.

These two classes are almost indistinguishable from two JS classes inherited by using extends in JavaScript. The only — very minor — difference is that when calling extended.tell() we will invoke Extended.prototype.tell() and not Extended.prototype.__proto__.tell()which is the same as Base.prototype.tell() . In our case the subclass will appear to have its own tell() method and the redirection to the base class method will happen at the C++ level.

Case 2: Extending EventEmitter

The other frequently encountered pattern is extending Node.js’ EventEmitter so that the C++ object can be compatible with the event API.

This one is a little bit uglier since we will have to call require from C++ —as EventEmitter is not defined unless one imports its definition.

There is nothing special about the declaration of the class itself, except that it will have to manually hold a reference to its JS parent:

class MyEmitter : public Napi::ObjectWrap<MyEmitter> {
public:
MyEmitter(const Napi::CallbackInfo &);

void Ping(const Napi::CallbackInfo &);

static Napi::Function GetClass(Napi::Env);

private:
// This is the quirk
static Napi::FunctionReference *EventEmitter;
};

When initially setting up the class, this reference must be initialized:

Napi::Function MyEmitter::GetClass(Napi::Env env) {
Napi::Function self =
DefineClass(env, "MyEmitter",
{
InstanceMethod("ping", &MyEmitter::Ping),
});

// require from C++ needs some acrobatics
// (the ugly part is in the JS file)
Napi::Function require = env.Global()
.Get("require").As<Napi::Function>();
Napi::Function ee = require.Call({Napi::String::New(env, "events")})
.ToObject()
.Get("EventEmitter")
.As<Napi::Function>();

// This is the visible inheritance
Napi::Function setProto = env.Global()
.Get("Object")
.ToObject()
.Get("setPrototypeOf")
.As<Napi::Function>();
// Same approach as the C++/C+ inheritance
// This makes instanceof work
setProto.Call({self, ee});
setProto.Call({self.Get("prototype"), ee.Get("prototype")});

// Keep a static reference to the constructor
MyEmitter::EventEmitter = new Napi::FunctionReference();
*MyEmitter::EventEmitter = Napi::Persistent(ee);
return self;
}

Now when creating object of this class, there will be no one to call the superclass constructor for you — remember that you are extending across the JS/C++ boundary — and neither the C++ compiler neither V8 know anything about this. You will have to do this yourself:

MyEmitter::MyEmitter(const Napi::CallbackInfo &info) : ObjectWrap(info) {
// Call the super class constructor
MyEmitter::EventEmitter->Call(this->Value(), {});
}

Your JS to C++ inheritance is now set up. When accessed from JS, MyEmitter will be next to indistinguishable from a JS class extended from EventEmitter . There is only one very ugly last caveat, you will have to add this line in the beginning of your JS code:

globalThis.require = require;

This will allow you to easily retrieve require from C++.

Calling your JS superclass methods from C++ will also be manual and it will need to go through V8. Here is an example of calling super.emit('data', 'pong') :

void MyEmitter::Ping(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
Napi::Object self = this->Value().ToObject();
Napi::Function emit = self.Get("emit").As<Napi::Function>();
emit.Call(this->Value(),
{Napi::String::New(env, "data"),
Napi::String::New(env, "pong")});
}

And of course, you could cache the first three lines in order to do this initialization only once.

My name is Momtchil Momtchev and I am an unemployed engineer with over 20 years of experience in the IT industry — mostly networking and low-level system engineering. For the last 6 years I have focused mostly on Node.js internals. I develop and maintain a number of binary Node.js addons.

I am at the center of a huge judicial scandal after an employer tried intervening in my sex life with the help of the French police, then tried extorting me to back off from suing him because it would expose his own problem. The affair has continued for many years now and has affected more than one open-source project.

--

--