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

一文看懂Node-Addon-Api的设计与实现

时间:2023-03-12 04:54:09 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。Nodej.jsAddon的开发方式一直在不断改进,并没有逐步完善。至少我们不用担心在升级Node.js版本时Addon不工作或重新编译。目前Node.js提供的开发方式是napi。但是napi使用起来非常冗余和繁琐,每一步都需要我们自己去控制,所以另外一个大佬封装了面向对象版本的api(node-addon-api),使用起来方便多了。本文分析的是node-addon-api的设计思路,但是不会分析太多细节,因为了解了设计思路之后,我们在使用的时候可以查阅文档或者看看源码。我们先看一个使用napi写helloworld的例子。#include#includestaticnapi_valueMethod(napi_envenv,napi_callback_infoinfo){napi_statusstatus;napi_valueworld;status=napi_create_string_utf8(env,"world",5,&world);断言(status==napi_ok);returnworld;}#defineDECLARE_NAPI_METHOD(name,func)\{name,0,func,0,0,0,napi_default,0}staticnapi_valueInit(napi_envenv,napi_valueexports){napi_statusstatus;napi_property_descriptordesc=DECLARE_NAPI_METHOD("你好",方法);status=napi_define_properties(env,exports,1,&desc);assert(status==napi_ok);returnexports;}NAPI_MODULE(NODE_GYP_MODULE_NAME,Init)接着我们看一下node-addon-api版本的写法。#includeNapi::StringMethod(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();returnNapi::String::New(env,"world");}Napi::ObjectInit(Napi::Envenv,Napi::Objectexports){exports.Set(Napi::String::New(env,"hello"),Napi::Function::New(env,Method));returnexports;}NODE_API_MODULE(hello,init)我们可以看到代码简洁了很多,有点写js的感觉。让我们来看看这些简单背后的设计。我们从模块定义开始分析。NODE_API_MODULE(hello,Init)NODE_API_MODULE是node-addon-api定义的宏。#defineNODE_API_MODULE(modname,regfunc)\staticnapi_value__napi_##regfunc(napi_envenv,napi_valueexports){\returnNapi::RegisterModule(env,exports,regfunc);\}\NAPI_MODULE(modname,__napi_##regfunc)我们看到NODE_API_MODULE用于LENAPI_MODUNAPI_MODULE的封装和分析可以参考之前napi原理相关的文章,这里不再具体分析。最后,加载插件时执行__napi_##regfunc函数。并传入napi_envenv,napi_valueexports参数。我们知道这是napi规范的一个参数。然后执行RegisterModule。inlinenapi_valueRegisterModule(napi_envenv,napi_valueexports,ModuleRegisterCallbackregisterCallback){//details::WrapCallback会执行lamda函数并返回lamdareturndetails::WrapCallback([&]{returnnapi_value(registerCallback(Napi::Env(env),Napi::Object(env,exports)));});}RegisterModule最终会执行registerCallback。我们看一下registerCallback变量的类型ModuleRegisterCallback的定义。typedefObject(*ModuleRegisterCallback)(Envenv,Objectexports);所以registerCallback的参数是Env和Object对象。这两个类不是Node.js或V8定义的,而是node-addon-api定义的。我们后面再分析,只要知道他是两个对象就可以了。这里registerCallback的值就是我们定义的Init函数。Napi::ObjectInit(Napi::Envenv,Napi::Objectexports){exports.设置(Napi::String::New(env,"hello"),Napi::Function::New(env,Method));returnexports;}通过set方法为exports定义属性,我们可以在js中访问对应的属性。最后返回exports,exports是Object类型。但是根据napi的接口定义。返回的类型应该是napi_value。让我们看看node-addon-api是如何做到的。让我们回到RegisterModule函数。returnnapi_value(registerCallback(Napi::Env(env),Napi::Object(env,exports)));我们看到registerCallback执行后的返回值会被转换为napi_value类型。那么Object类型是如何自动转换为napi_value类型的呢?我们稍后再分析。了解了node-addon-api的用法后,我们开始详细分析设计。我们先来看看Env的设计。classEnv{public:Env(napi_envenv);operatornapi_env()const;private:napi_env_env;};inlineEnv::Env(napi_envenv):_env(env){}//类型重载inlineEnv::operatornapi_env()const{return_env;}我们只看核心设计,忽略一些无关紧要的细节。我们看到Env的设计很简单,就是对napi的napi_env的封装。接下来我们看看设计的类型。classValue{public:Value();Value(napi_envenv,napi_valuevalue);operatornapi_value()const;Napi::EnvEnv()const;protected:napi_env_env;napi_value_value;};Value是node-addon-api的类型基类,类似到V8的设计。我们看到Value中只有两个字段,env和_value。env就是我们刚才说的Env。_value是对napi类型的封装。Value类只是抽象封装,不涉及具体逻辑。下面以自定义Init函数为例,开始分析具体逻辑。Napi::ObjectInit(Napi::Envenv,Napi::Objectexports){exports.设置(Napi::String::New(env,"hello"),Napi::Function::New(env,Method));returnexports;}让我们先看看String::New的实现。className:publicValue{public:Name();Name(napi_envenv,napi_valuevalue);};classString:publicName{public:staticStringNew(napi_envenv,constchar*value);};inlineStringString::New(napi_envenv,constchar*val){napi_valuevalue;napi_statusstatus=napi_create_string_utf8(env,val,std::strlen(val),&value);NAPI_THROW_IF_FAILED(env,status,String());returnString(env,value);}我们看到New的实现很简单,主要是用于napi包。但有些细节还是需要注意的。1我们看到exports.Set函数的第一个参数是Env类型,而New函数的第一个参数类型是napi_env,看起来不兼容。这是如何自动转换的?因为Env类重载了napi_env类型。inlineEnv::operatornapi_env()const{return_env;}我们看到当需要napi_env类型时,Env会返回_env,而_env就是napi_env类型。2通过napi接口创建值后,最后返回一个String类型。让我们看一下String构造函数。inlineString::String(napi_envenv,napi_valuevalue):Name(env,value){}inlineName::Name(napi_envenv,napi_valuevalue):Value(env,value){}最后调用Value构造函数保存napi返回的值。并将一个String对象返回给调用者。让我们看看在exports.Set(Napi::String::New(env,"hello"),Napi::Function::New(env,Method))时如何使用这个String对象。出口是一个对象。Object和String的实现类似。它们都继承了Value类,并在内部封装了napi_env和napi_value变量。那么让我们看看Object::Set的实现。templateinlineboolObject::Set(napi_valuekey,constValueType&value){napi_statusstatus=napi_set_property(_env,_value,key,Value::From(_env,value));NAPI_THROW_IF_FAILED(_env,status,false);returntrue;}_valuevalue它是Object封装的napi_value对象,是一个V8Object对象。然后通过napi_set_property设置对象的属性和值。同样,我们发现Set函数的实参是一个String对象,但是类型参数是napi_value类型。这个类似于Env的自动转换,String继承Value,Value重载类型napi_value。inlineValue::operatornapi_value()const{return_value;}返回封装的napi_value变量。我们通过Set设置了一个属性hello,value是一个函数。Napi::StringMethod(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();returnNapi::String::New(env,"world");}我们在js层调用hello的时候不会执行这个函数,先执行node-addon-api的代码,直到node-addon-api封装了napi的变量才会调用该方法。所以我们看到Method的入参类型和napi是不一样的。最后,当Method执行完毕返回时,也是先返回到node-addon-api。node-addon-api将Method(String对象)的返回值转换成napi格式(napi_value)再返回给napi(这个比较复杂,暂不深入分析)。至此我们已经看到了如图所示的node-addon-api设计的基本思路。大意是node-addon-api给我们封装了一层。napi在调用我们定义的内容时,会先经过node-addon-api。node-addon-api封装了napi的输入参数,然后调用我们自定义的内容。同样的,当我们返回内容给napi时,在返回给napi之前会被node-addon-api打包。比如我们在addon中创建一个数字时,会执行NumberNew(napi_envenv,doublevalue);New会调用napi的napi_create_double创建一个napi_value变量。然后把napi_value的值封装成Number,最后返回一个Number给我们。后面我们调用Number的其他方法时,node-addon-api会从Number对象中获取保存的napi_value的值,然后调用napi的API。这样,我们只需要面对node-addon-api提供的接口,而无需了解napi。另外node-addon-api也做了一些运算符重载,方便我们写代码。比如Object[]的重载。Valueoperator[](constchar*utf8name)const;让我们看看实现。inlineValueObject::operator[](constchar*utf8name)const{returnGet(utf8name);}inlineValueObject::Get(constchar*utf8name)const{napi_valueresult;napi_statusstatus=napi_get_named_property(_env,_value,utf8name,&result);NAPI_THROW_IF_FAILED(_env,status,Value());returnValue(_env,result);}这样我们就可以通过obj['name']来访问对象了。否则,我们还需要通过以下方式访问它。napi_valuevalue;napi_statusstatus=napi_get_named_property(_env,_value,key,&value);如果这样的代码很多,会很麻烦,效率也很低。另外,node-addon-api进行了大量的类型重载,使得变量的类型转换可以自动进行,无需强制转换。比如我们可以直接执行下面的代码。int32_tnum=数字对象;因为Number重载了int32_t。inlineNumber::operatorint32_t()const{returnInt32Value();}inlineint32_tNumber::Int32Value()const{int32_tresult;napi_statusstatus=napi_get_value_int32(_env,_value,&result);NAPI_THROW_IF_FAILED(_env,status,0);returnresult;}分析了实现原理和node-addon-api的思路,实现的代码近万行。虽然类似的逻辑很多,但是也有一些比较复杂的封装。感兴趣的同学可以自行阅读。.