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

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

#include <assert.h>
#include <node_api.h>static napi_value Method(napi_env env, napi_callback_info info) {napi_status status;napi_value world;status = napi_create_string_utf8(env, "world", 5, &world);assert(status == napi_ok);return world;
}#define DECLARE_NAPI_METHOD(name, func)                                        \{ name, 0, func, 0, 0, 0, napi_default, 0 }static napi_value Init(napi_env env, napi_value exports) {napi_status status;napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);status = napi_define_properties(env, exports, 1, &desc);assert(status == napi_ok);return exports;
}NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

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

#include <napi.h>Napi::String Method(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();return Napi::String::New(env, "world");
}Napi::Object Init(Napi::Env env, Napi::Object exports) {exports.Set(Napi::String::New(env, "hello"),Napi::Function::New(env, Method));return exports;
}NODE_API_MODULE(hello, Init)

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

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

NODE_API_MODULE(hello, Init)

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

#define NODE_API_MODULE(modname, regfunc)                                      \static napi_value __napi_##regfunc(napi_env env, napi_value exports) {       \return Napi::RegisterModule(env, exports, regfunc);                        \}                                                                            \NAPI_MODULE(modname, __napi_##regfunc)

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

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

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

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

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

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

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

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

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

我们先看看Env的设计。

class Env {public:Env(napi_env env);operator napi_env() const;private:napi_env _env;
};inline Env::Env(napi_env env) : _env(env) {}// 类型重载
inline Env::operator napi_env() const {return _env;
}

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

class Value {public:Value();     Value(napi_env env,  napi_value value);  operator napi_value() const;Napi::Env Env() const;protected:napi_env _env;napi_value _value;
};

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

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

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

class Name : public Value {public:Name();                     Name(napi_env env, napi_value value);
};class String : public Name {public:static String New(napi_env env, const char* value);
};inline String String::New(napi_env env, const char* val) {napi_value value;napi_status status = napi_create_string_utf8(env, val, std::strlen(val), &value);NAPI_THROW_IF_FAILED(env, status, String());return String(env, value);
}

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

inline Env::operator napi_env() const {return _env;
}

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

inline String::String(napi_env env, napi_value value) : Name(env, value) {}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的实现。

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

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

inline Value::operator napi_value() const {return _value;
}

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

Napi::String Method(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();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 []的重载。

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

我们看看实现。

inline Value Object::operator [](const char* utf8name) const {return Get(utf8name);
}inline Value Object::Get(const char* utf8name) const {napi_value result;napi_status status = napi_get_named_property(_env, _value, utf8name, &result);NAPI_THROW_IF_FAILED(_env, status, Value());return Value(_env, result);
}

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

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

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

int32_t num = Number对象;

因为Number对int32_t进行了重载。

inline Number::operator int32_t() const {return Int32Value();
}inline int32_t Number::Int32Value() const {int32_t result;napi_status status = napi_get_value_int32(_env, _value, &result);NAPI_THROW_IF_FAILED(_env, status, 0);return result;
}

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

node-addon-api的设计和实现相关推荐

  1. node.js api接口_如何在Node.js API客户端中正常处理故障

    node.js api接口 by Roger Jin 罗杰·金(Roger Jin) 如何在Node.js API客户端中正常处理故障 (How to gracefully handle failur ...

  2. 【Node】一个完整的 node addon 实现流程

    背景介绍 为什么要写 node addon 试想这样一种场景:我们想在 js 层实现某个业务场景,但是这套业务逻辑已经有存在的 C++ 版本了,这个时候我们有两个选择 重新实现一套在 JS 版本的业务 ...

  3. Node.js API参考文档(目录)

    Node.js v11.5.0 API参考文档 Node.js®是基于Chrome的V8 JavaScript引擎构建的JavaScript运行时. 关于文档 用法和示例 断言测试 稳定性:2 - 稳 ...

  4. REST API URI 设计的七准则

    在了解 REST API URI 设计的规则之前,让我们快速过一下我们将要讨论的一些术语. URI REST API 使用统一资源标识符(URI)来寻址资源.在今天的网站上,URI 设计范围从可以清楚 ...

  5. 关于API的设计和需求抽象

    一,先来谈抽象吧,因为抽象跟后面的API的设计是息息相关的 有句话说的好(不知道谁说的了):计算机科学中的任何问题都可以抽象出一个中间层就解决了. 抽象是指在思维中对同类事物去除其现象的.次要的方面, ...

  6. 从涂鸦到发布 —— 理解API的设计过程

    要想设计出可以正常运行的Web API,对基于web的应用的基本理解是一个良好的基础.但如果你的目标是创建出优秀的API,那么仅凭这一点还远远不够.设计优秀的API是一个艰难的过程,如果它恰巧是你当前 ...

  7. 优秀的API接口设计原则及方法

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  8. 后端:REST API URI 设计的七准则

             正文    在了解 REST API URI 设计的规则之前,让我们快速过一下我们将要讨论的一些术语. URI REST API 使用统一资源标识符(URI)来寻址资源.在今天的网站 ...

  9. 架构必备「RESTful API」设计技巧经验总结

    转载自   架构必备「RESTful API」设计技巧经验总结 [译者注]本文是作者在自己的工作经验中总结出来的RESTful API设计技巧,虽然部分技巧仍有争议,但总体来说还是有一定的参考价值的. ...

  10. 【HAVENT原创】Node Express API 通用配置

    为什么80%的码农都做不了架构师?>>>    ( 基于 Express 4.x ) 启动文件 /app.js: var express = require('express'); ...

最新文章

  1. 阿里云盾技术强在哪里?轻松防御DDoS、CC攻击
  2. 栈上对象的内存自动释放
  3. C#和Java详细描述
  4. 用友软件动态密码安全认证解决方案
  5. 如何在六个月或更短的时间内成为DevOps工程师(二):配置
  6. V 神呼吁宽大处理,以太坊开发者 Virgil Griffith 被判入狱 63 个月
  7. python主要用来做什么-python语言都可以做什么
  8. vue一个页面发出多个异步请求_vue(6)—— vue中向后端异步请求
  9. 大数据将会带来什么机遇
  10. 【渝粤教育】国家开放大学2018年春季 3818-21T燃气工程施工 参考试题
  11. 【python】使用in判断元素是否在列表(list)中,如何提升搜索效率?
  12. django基础(四)详解Views视图层
  13. 转(Google 全国 地图 纠偏数据 偏移数据 火星坐标修正 方案 )
  14. matlab实时编辑器怎么用,Markdown 实时编辑器
  15. 博士生为什么纷纷逃离科研?
  16. java计算机毕业设计健身房管理系统演示录像2021MyBatis+系统+LW文档+源码+调试部署
  17. 这可能是我用过最好用的SQL工具,免费还免安装,良心推荐SQL Studio
  18. OBS Studio录屏黑屏解决办法win10
  19. 计算机图形物理知识,计算机图形学:虚拟和现实世界的融合
  20. 北京上市公司招聘.net架构师及开发人员

热门文章

  1. js 正则 验证密码输入,必须为6-15位,含有数字字母,或者符号
  2. Win10系统台式机如何调节系统亮度
  3. unity android 30帧,解除某Unity游戏的30帧帧率限制
  4. vue 多种方法实现名字拼接
  5. Joel Spolsky在耶鲁的演讲无责任导读
  6. javaweb-day03-7(基础加强-泛型)
  7. 圣邦微电子2023校招笔试
  8. mysql57配置教程
  9. Nice UI - Hacked.io
  10. 什么是OsmocomBB