当前位置: 首页 > 科技观察

使用Node.jsAddon实现类继承

时间:2023-03-16 22:32:52 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。前言:昨天有同学问如何通过NAPI将C++类的继承关系映射到JS。可惜NAPI好像不支持,但是V8支持,因为V8在头文件中导出了这些API,Node.js也支持。依赖这些API,所以可以说是比较稳定。本文介绍如何实现这种映射(不确定是否能满足这位同学的需求)。下面我们看一下Addon的实现。会涉及到一些V8的使用,大家可以先看这篇《一段js理解nodejs中js调用c++/c的过程》。先看基类的实现。#ifndefBASE_H#defineBASE_H#include#include#includeusingnamespacenode;usingnamespacev8;classBase:publicObjectWrap{public:staticvoidNew(constFunctionCallbackInfo&info){//创建一个新对象,然后包装成info.This(),后面会使用Base*base=newBase();base->Wrap(info.This());}staticvoidPrint(constFunctionCallbackInfo&info){//Unpack出来使用Base*base=ObjectWrap::Unwrap(info.This());base->print();}voidprint(){printf("baseprint\n");}voidhello(){printf("basehello\n");}};#endifNode.js提供的ObjectWrap类实现了Wrap和UnWrap的功能,所以我们可以继承它来简化数据包拆包的逻辑。Base类定义了两个功能函数hello和print,定义了两个类静态函数New和Print。New函数是核心逻辑。这个函数会在js层执行newBase的时候执行,会传入一个对象,这时候我们先创建一个真实有用的对象,通过Wrap把这个对象包装到传入的对象中。让我们继续讨论子类。#ifndefDERIVED_H#defineDERIVED_H#include#include#include"Base.h"usingnamespacenode;usingnamespacev8;classDerived:publicBase{public:staticvoidNew(constFunctionCallbackInfo&info){Derived*derived(=newDer);derived->Wrap(info.This());}staticvoidHello(constFunctionCallbackInfo&info){Derived*derived=ObjectWrap::Unwrap(info.This());//调用函数基类derived->hello();}};#endif子类的逻辑类似。New函数的逻辑与基类相同。除了继承基类的方法外,还额外定义了一个Hello函数,但是我们看到这只是一个shellSub,底层还是调用了基类的函数。定义好基类和子类后,我们将这两个类导出到JS中。#include#include"Base.h"#include"Derived.h"namespacedemo{usingv8::FunctionCallbackInfo;usingv8::Isolate;usingv8::Local;usingv8::Object;usingv8::String;usingv8::值;usingv8::FunctionTemplate;usingv8::Function;usingv8::Number;usingv8::MaybeLocal;usingv8::Context;usingv8::Int32;usingv8::NewStringType;voidInitialize(本地<对象>导出,本地<值>module,Localcontext){Isolate*isolate=context->GetIsolate();//创建两个函数模板,基类和子类,当js层导出的函数New时,V8会执行New函数并传递inAnobjectLocalbase=FunctionTemplate::New(isolate,Base::New);Localderived=FunctionTemplate::New(isolate,Derived::New);//js使用的类名层NewStringTypetype=NewStringType::kNormal;Localbase_string=String::NewFromUtf8(isolate,"Base",type).ToLocalChecked();Localderived_string=String::NewFromUtf8(isolate,"Derived",type).ToLocalChecked();//预留一个poi空间基础->InstanceTemplate()->SetInternalFieldCount(1);derived->InstanceTemplate()->SetInternalFieldCount(1);//为属性的值定义两个函数模板LocalBasePrint=FunctionTemplate::New(isolate,Base::Print);LocalHello=FunctionTemplate::New(isolate,Derived::Hello);//为基类定义一个打印函数base->PrototypeTemplate()->Set(isolate,"print",BasePrint);//为子类定义一个hello函数derived->PrototypeTemplate()->Set(isolate,"hello",Hello);//建立继承关系derived->Inherit(base);//导出两个函数为js层exports->Set(context,base_string,base->GetFunction(context).ToLocalChecked()).Check();exports->Set(context,derived_string,derived->GetFunction(context).ToLocalChecked()).Check();}NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME,Initialize)}我们看到为基类原型定义了一个print函数,为子类定义了一个hello函数。最后看看在JS层如何使用。const{Base,Derived}=require('./build/Release/test.node');constbase=newBase();constderived=newDerived();base.print();derived.hello();derived.print();console.log(derivedinstanceofBase,derivedinstanceofDerived)下面是具体的输出baseprintbasehellobaseprinttruetrue我们逐句分析1base.print()比较简单,就是调用基类定义的Print函数。2derived.hello()看似调用了子类的Hello函数,但是在Hello函数中调用了基类的hello函数,实现了逻辑复用。3derived.print()子类没有实现print函数,这里调用基类的print函数,同1。4Base的派生实例,Derived的派生实例。根据我们的定义,derived不仅是Derived的一个实例,还是Base的一个实例。经过对实现代码的分析,我们可以看出,将C++类映射到JS有两种方式。第一个是两个C++类没有继承关系,两个在JS层有继承关系的类(函数)是通过V8的继承API实现的,比如print函数的实现,我们看到子类没有实现print,但是可以调用print,因为基类定义了它,Node.js是这样处理的。第二种是两个具有继承关系的C++类。这两个继承的类也是通过V8API导出到JS的,因为JS层只用到了shell。当C++代码真正执行时,我们会反映出来。脱离了这种继承关系。比如Hello函数的实现,虽然我们在子类中导出了hello函数,而JS执行hello时确实执行了子类的C++代码,但是最终还是调用了基类的hello函数。最后我们看看如何通过Nodej.js来做这个映射,通过PipeWrap.cc的实现来分析一下。//新建一个函数模板Localt=env->NewFunctionTemplate(New);//继承两个函数模板t->Inherit(LibuvStreamWrap::GetConstructorTemplate(env));//使用exports导出到JS->Set(env->context(),pipeString,t->GetFunction(env->context()).ToLocalChecked()).Check();上面的代码实现了继承,我们来看一下GetConstructorTemplate的实现。tmpl=env->NewFunctionTemplate(nullptr);env->SetProtoMethod(tmpl,"setBlocking",SetBlocking);env->SetProtoMethod(tmpl,"readStart",JSMethod<&StreamBase::ReadStartJS>);env->SetProtoMethod(t,"readStop",JSMethod<&StreamBase::ReadStopJS>);//...以上代码创建了一个新的函数模板,并设置了一系列原型属性,然后模板t继承了这些属性。让我们看看它是如何在Node.js中使用的。functioncreateHandle(fd,is_server){//...returnnewPipe(is_server?PipeConstants.SERVER:PipeConstants.SOCKET);}this._handle=createHandle(fd,false);err=this._handle.setBlocking(true);上面的代码首先创建了一个Pipe对象,然后调用了它的setBlocking方法。我们发现Pipe(pipe_wrap.cc)并没有实现setBlocking函数,但是为什么他可以调用setBlocking呢?答案是它的基类已经实现了。后记:在JS中实现继承很简单,但是在底层实现起来还是比较复杂,但是从代码设计的角度来说是非常有必要的。该代码在存储库中可用:https://github.com/theanarkh/learn-to-write-nodejs-addons。