一篇了解Node-Addon-Api的设计和实现

开发 前端
开发Nodej.js Addon的方式经过不断地改进,已经非逐步完善,至少我们不需要在升级Node.js版本的同时担心Addon用不了或者重新编译。目前Node.js提供的开发方式是napi。

[[411323]]

本文转载自微信公众号「编程杂技」,作者theanarkh 。转载本文请联系编程杂技公众号。

开发Nodej.js Addon的方式经过不断地改进,已经非逐步完善,至少我们不需要在升级Node.js版本的同时担心Addon用不了或者重新编译。目前Node.js提供的开发方式是napi。但是napi用起来非常冗余和麻烦,每一步都需要我们自己去控制,所以又有大佬封装了面向对象版本的api(node-addon-api),使用上方便了很多,本文分析一下node-addon-api的设计思想,但不会分析过多细节,因为我们理解了设计思想后,使用时去查阅文档或者看源码就可以。

我们首先看一下使用napi写一个hello world的例子。

  1. #include <assert.h> 
  2. #include <node_api.h> 
  3. static napi_value Method(napi_env env, napi_callback_info info) { 
  4.   napi_status status; 
  5.   napi_value world; 
  6.   status = napi_create_string_utf8(env, "world", 5, &world); 
  7.   assert(status == napi_ok); 
  8.   return world; 
  9.  
  10. #define DECLARE_NAPI_METHOD(name, func)                                        \ 
  11.   { name, 0, func, 0, 0, 0, napi_default, 0 } 
  12.  
  13. static napi_value Init(napi_env env, napi_value exports) { 
  14.   napi_status status; 
  15.   napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method); 
  16.   status = napi_define_properties(env, exports, 1, &desc); 
  17.   assert(status == napi_ok); 
  18.   return exports; 
  19.  
  20. NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 

接着我们看一下node-addon-api版的写法。

  1. #include <napi.h> 
  2.  
  3. Napi::String Method(const Napi::CallbackInfo& info) { 
  4.   Napi::Env env = info.Env(); 
  5.   return Napi::String::New(env, "world"); 
  6.  
  7. Napi::Object Init(Napi::Env env, Napi::Object exports) { 
  8.   exports.Set(Napi::String::New(env, "hello"), 
  9.               Napi::Function::New(env, Method)); 
  10.   return exports; 
  11.  
  12. NODE_API_MODULE(hello, Init) 

我们看到,代码简洁了很多,有点写js的感觉了。

下面我们看看这些简洁背后的设计。我们从模块定义开始分析。

  1. NODE_API_MODULE(hello, Init) 

NODE_API_MODULE是node-addon-api定义的宏。

  1. #define NODE_API_MODULE(modname, regfunc)                                      \ 
  2.   static napi_value __napi_##regfunc(napi_env env, napi_value exports) {       \ 
  3.     return Napi::RegisterModule(env, exports, regfunc);                        \ 
  4.   }                                                                            \ 
  5.   NAPI_MODULE(modname, __napi_##regfunc) 

我们看到NODE_API_MODULE是对NAPI_MODULE的封装,NAPI_MODULE的分析可以参考之前napi原理相关的文章,这里就不具体分析。最后在加载addon的时候执行__napi_##regfunc函数。并传入napi_env env, napi_value exports参数。我们知道这是napi规范的参数。接着执行RegisterModule。

  1. inline napi_value RegisterModule(napi_env env, 
  2.                                  napi_value exports, 
  3.                                  ModuleRegisterCallback registerCallback) { 
  4.   // details::WrapCallback里会执行lamda函数并返回lamda的返回值                       
  5.   return details::WrapCallback([&] { 
  6.     return napi_value(registerCallback(Napi::Env(env), 
  7.                                        Napi::Object(env, exports))); 
  8.   }); 

RegisterModule里最终会执行registerCallback。我们看一下registerCallback变量的类型ModuleRegisterCallback的定义。

  1. typedef Object (*ModuleRegisterCallback)(Env env, Object exports); 

所以registerCallback的参数是Env和Object对象。这两个类不是Node.js也不是V8定义的,而是node-addon-api。我们一会再分析,我们先知道他是两个对象就好。这里registerCallback的值是我们定义的Init函数。

  1. Napi::Object Init(Napi::Env env, Napi::Object exports) { 
  2.   exports.Set(Napi::String::New(env, "hello"), 
  3.               Napi::Function::New(env, Method)); 
  4.   return exports; 

通过Set方法给exports定义属性,我们在js就可以访问对应的属性了。最后返回exports,exports是Object类型。但根据napi的接口定义。返回的类型应该是napi_value。我们看看node-addon-api是怎么做的。我们回到RegisterModule函数。

  1. return napi_value(registerCallback(Napi::Env(env),  Napi::Object(env, exports))); 

我们看到registerCallback执行后的返回值会被转成napi_value类型。那么Object类型是怎么自动转成napi_value类型的呢?我们一会分析。了解了node-addon-api的使用方式后,我们开始具体分析其中的设计。

我们先看看Env的设计。

  1. class Env { 
  2.   public
  3.     Env(napi_env env); 
  4.     operator napi_env() const; 
  5.  
  6.   private: 
  7.     napi_env _env; 
  8. }; 
  9.  
  10. inline Env::Env(napi_env env) : _env(env) {} 
  11.  
  12. // 类型重载 
  13. inline Env::operator napi_env() const { 
  14.   return _env; 

我们只看核心的设计,忽略一些无关重要的细节。我们看到Env的设计很简单,就是对napi的napi_env的封装。接着我们看类型的设计。

  1. class Value { 
  2.   public
  3.     Value();      
  4.     Value(napi_env env,  napi_value value);   
  5.     operator napi_value() const; 
  6.     Napi::Env Env() const; 
  7.  
  8.   protected: 
  9.     napi_env _env; 
  10.     napi_value _value; 
  11. }; 

Value是node-addon-api的类型基类,类似V8里的设计。我们看到Value里面只有两个字段,env和_value。env就是我们刚才提到的Env。_value就是对napi类型的封装。Value类只是抽象的封装,不涉及到具体的逻辑。下面我们以自定义的Init函数为例,开始分析具体的逻辑。

  1. Napi::Object Init(Napi::Env env, Napi::Object exports) { 
  2.   exports.Set(Napi::String::New(env, "hello"),  
  3.               Napi::Function::New(env, Method) 
  4.              ); 
  5.   return exports; 

我们先看看String::New的实现。

  1. class Name : public Value { 
  2.   public
  3.     Name();                      
  4.     Name(napi_env env, napi_value value);  
  5. }; 
  6.  
  7. class String : public Name { 
  8.   public
  9.    static String New(napi_env env, const char* value); 
  10. }; 
  11.  
  12. inline String String::New(napi_env env, const char* val) { 
  13.       napi_value value; 
  14.       napi_status status = napi_create_string_utf8(env, val, std::strlen(val), &value); 
  15.       NAPI_THROW_IF_FAILED(env, status, String()); 
  16.       return String(env, value); 

我们看到New的实现很简单,主要是对napi的封装。但有些细节还是需要注意的。1 我们看到exports.Set函数的第一个参数是Env类型,但是New函数的第一个参数类型是napi_env,看起来不兼容。这个是如何自动转换的呢?因为Env类对napi_env类型进行了重载。

  1. inline Env::operator napi_env() const { 
  2.   return _env; 

我们看到当需要napi_env类型的时候,Env会返回_env,_env就是napi_env类型。2 通过napi接口创建了值之后,最后返回的是一个String类型。我们看看String构造函数。

  1. inline String::String(napi_env env, napi_value value) : Name(env, value) {} 
  2. inline Name::Name(napi_env env, napi_value value) : Value(env, value) {} 

最后调用Value构造函数保存了napi返回的值。并且给调用方返回了一个String对象。我们看看exports.Set(Napi::String::New(env, "hello"), Napi::Function::New(env, Method))的时候是如何使用这个String对象的。exports是一个Object。Object和String的实现是类似的,他们都是继承Value类,在内部封装了napi_env和napi_value变量。所以我们看看Object::Set的实现。

  1. template <typename ValueType> 
  2. inline bool Object::Set(napi_value key, const ValueType& value) { 
  3.   napi_status status = napi_set_property(_env, _value, key, Value::From(_env, value)); 
  4.   NAPI_THROW_IF_FAILED(_env, status, false); 
  5.   return true

_value的值是Object封装的napi_value对象,也就是一个V8 Object对象。然后通过napi_set_property设置对象的属性和值。同样我们发现Set函数的实参是String对象,但是型参是napi_value类型。这个和Env的自动转换是类似的,String继承了Value,而Value重载了类型napi_value。

  1. inline Value::operator napi_value() const { 
  2.   return _value; 

即返回了封装的napi_value变量。我们通过Set设置了一个属性hello,值是一个函数。

  1. Napi::String Method(const Napi::CallbackInfo& info) { 
  2.   Napi::Env env = info.Env(); 
  3.   return Napi::String::New(env, "world"); 

当我们在js层调用hello的时候,不会执行这个函数,而是先执行node-addon-api的代码,node-addon-api对napi的变量进行封装后,才会调用Method。所以我们看到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时,也会经过node-addon-api的封装再回到napi。比如我们在addon里创建一个数字时, 我们会执行Number New(napi_env env, double value);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 []的重载。

  1. Value operator []( const char* utf8name) const; 

我们看看实现。

  1. inline Value Object::operator [](const char* utf8name) const { 
  2.   return Get(utf8name); 
  3.  
  4. inline Value Object::Get(const char* utf8name) const { 
  5.   napi_value result; 
  6.   napi_status status = napi_get_named_property(_env, _value, utf8name, &result); 
  7.   NAPI_THROW_IF_FAILED(_env, status, Value()); 
  8.   return Value(_env, result); 

这样我们就可以通过obj['name']这种方式访问对象了。否则我们还需要像下面的方式访问。

  1. napi_value value; 
  2. napi_status status = napi_get_named_property(_env, _value, key, &value); 

如果大量这样的代码将会非常麻烦和低效。另外node-addon-api对类型进行了大量的重载,使得变量的类型转换得以自动进行不需要强制转换来转换去。比如我们可以直接执行以下代码。

  1. int32_t num = Number对象; 

因为Number对int32_t进行了重载。

  1. inline Number::operator int32_t() const { 
  2.   return Int32Value(); 
  3.  
  4. inline int32_t Number::Int32Value() const { 
  5.   int32_t result; 
  6.   napi_status status = napi_get_value_int32(_env, _value, &result); 
  7.   NAPI_THROW_IF_FAILED(_env, status, 0); 
  8.   return result; 

后记:本文大致分析了node-addon-api的实现原理和思想,实现的代码将近万行,虽然有很多类似的逻辑,但是也有些比较复杂的封装,有兴趣的同学可自行阅读。.

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-11-24 08:51:32

Node.js监听函数

2021-07-03 08:04:10

io_uringNode.js异步IO

2021-12-30 09:38:51

DDoS攻击防范

2022-10-26 07:39:36

MVCC数据库RR

2022-12-19 08:14:30

注解开发配置

2021-05-20 06:57:16

RabbitMQ开源消息

2022-05-06 23:03:48

V8CPUProfiler

2023-10-17 08:15:28

API前后端分离

2021-08-11 07:02:21

npm包管理器工具

2021-07-16 04:56:03

NodejsAddon

2022-05-06 13:30:56

TDD场景代码

2021-07-14 10:08:30

责任链模式加工链

2021-10-28 19:15:02

IPUARM

2021-07-10 09:02:42

编程语言 TypeScript

2020-10-09 08:15:11

JsBridge

2022-02-18 08:54:21

docker操作系统Linux

2021-07-02 08:51:28

Vite线程项目

2022-05-05 07:40:07

maskCSS

2023-05-12 08:19:12

Netty程序框架

2021-07-28 10:02:54

建造者模式代码
点赞
收藏

51CTO技术栈公众号