一篇了解Node-Addon-Api的設(shè)計(jì)和實(shí)現(xiàn)
本文轉(zhuǎn)載自微信公眾號(hào)「編程雜技」,作者theanarkh 。轉(zhuǎn)載本文請聯(lián)系編程雜技公眾號(hào)。
開發(fā)Nodej.js Addon的方式經(jīng)過不斷地改進(jìn),已經(jīng)非逐步完善,至少我們不需要在升級(jí)Node.js版本的同時(shí)擔(dān)心Addon用不了或者重新編譯。目前Node.js提供的開發(fā)方式是napi。但是napi用起來非常冗余和麻煩,每一步都需要我們自己去控制,所以又有大佬封裝了面向?qū)ο蟀姹镜腶pi(node-addon-api),使用上方便了很多,本文分析一下node-addon-api的設(shè)計(jì)思想,但不會(huì)分析過多細(xì)節(jié),因?yàn)槲覀兝斫饬嗽O(shè)計(jì)思想后,使用時(shí)去查閱文檔或者看源碼就可以。
我們首先看一下使用napi寫一個(gè)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)
我們看到,代碼簡潔了很多,有點(diǎn)寫js的感覺了。
下面我們看看這些簡潔背后的設(shè)計(jì)。我們從模塊定義開始分析。
- 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原理相關(guān)的文章,這里就不具體分析。最后在加載addon的時(shí)候執(zhí)行__napi_##regfunc函數(shù)。并傳入napi_env env, napi_value exports參數(shù)。我們知道這是napi規(guī)范的參數(shù)。接著執(zhí)行RegisterModule。
- inline napi_value RegisterModule(napi_env env,
- napi_value exports,
- ModuleRegisterCallback registerCallback) {
- // details::WrapCallback里會(huì)執(zhí)行l(wèi)amda函數(shù)并返回lamda的返回值
- return details::WrapCallback([&] {
- return napi_value(registerCallback(Napi::Env(env),
- Napi::Object(env, exports)));
- });
- }
RegisterModule里最終會(huì)執(zhí)行registerCallback。我們看一下registerCallback變量的類型ModuleRegisterCallback的定義。
- typedef Object (*ModuleRegisterCallback)(Env env, Object exports);
所以registerCallback的參數(shù)是Env和Object對象。這兩個(gè)類不是Node.js也不是V8定義的,而是node-addon-api。我們一會(huì)再分析,我們先知道他是兩個(gè)對象就好。這里registerCallback的值是我們定義的Init函數(shù)。
- 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就可以訪問對應(yīng)的屬性了。最后返回exports,exports是Object類型。但根據(jù)napi的接口定義。返回的類型應(yīng)該是napi_value。我們看看node-addon-api是怎么做的。我們回到RegisterModule函數(shù)。
- return napi_value(registerCallback(Napi::Env(env), Napi::Object(env, exports)));
我們看到registerCallback執(zhí)行后的返回值會(huì)被轉(zhuǎn)成napi_value類型。那么Object類型是怎么自動(dòng)轉(zhuǎn)成napi_value類型的呢?我們一會(huì)分析。了解了node-addon-api的使用方式后,我們開始具體分析其中的設(shè)計(jì)。
我們先看看Env的設(shè)計(jì)。
- 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;
- }
我們只看核心的設(shè)計(jì),忽略一些無關(guān)重要的細(xì)節(jié)。我們看到Env的設(shè)計(jì)很簡單,就是對napi的napi_env的封裝。接著我們看類型的設(shè)計(jì)。
- 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里的設(shè)計(jì)。我們看到Value里面只有兩個(gè)字段,env和_value。env就是我們剛才提到的Env。_value就是對napi類型的封裝。Value類只是抽象的封裝,不涉及到具體的邏輯。下面我們以自定義的Init函數(shù)為例,開始分析具體的邏輯。
- 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的實(shí)現(xiàn)。
- 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的實(shí)現(xiàn)很簡單,主要是對napi的封裝。但有些細(xì)節(jié)還是需要注意的。1 我們看到exports.Set函數(shù)的第一個(gè)參數(shù)是Env類型,但是New函數(shù)的第一個(gè)參數(shù)類型是napi_env,看起來不兼容。這個(gè)是如何自動(dòng)轉(zhuǎn)換的呢?因?yàn)镋nv類對napi_env類型進(jìn)行了重載。
- inline Env::operator napi_env() const {
- return _env;
- }
我們看到當(dāng)需要napi_env類型的時(shí)候,Env會(huì)返回_env,_env就是napi_env類型。2 通過napi接口創(chuàng)建了值之后,最后返回的是一個(gè)String類型。我們看看String構(gòu)造函數(shù)。
- inline String::String(napi_env env, napi_value value) : Name(env, value) {}
- inline Name::Name(napi_env env, napi_value value) : Value(env, value) {}
最后調(diào)用Value構(gòu)造函數(shù)保存了napi返回的值。并且給調(diào)用方返回了一個(gè)String對象。我們看看exports.Set(Napi::String::New(env, "hello"), Napi::Function::New(env, Method))的時(shí)候是如何使用這個(gè)String對象的。exports是一個(gè)Object。Object和String的實(shí)現(xiàn)是類似的,他們都是繼承Value類,在內(nèi)部封裝了napi_env和napi_value變量。所以我們看看Object::Set的實(shí)現(xiàn)。
- 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對象,也就是一個(gè)V8 Object對象。然后通過napi_set_property設(shè)置對象的屬性和值。同樣我們發(fā)現(xiàn)Set函數(shù)的實(shí)參是String對象,但是型參是napi_value類型。這個(gè)和Env的自動(dòng)轉(zhuǎn)換是類似的,String繼承了Value,而Value重載了類型napi_value。
- inline Value::operator napi_value() const {
- return _value;
- }
即返回了封裝的napi_value變量。我們通過Set設(shè)置了一個(gè)屬性hello,值是一個(gè)函數(shù)。
- Napi::String Method(const Napi::CallbackInfo& info) {
- Napi::Env env = info.Env();
- return Napi::String::New(env, "world");
- }
當(dāng)我們在js層調(diào)用hello的時(shí)候,不會(huì)執(zhí)行這個(gè)函數(shù),而是先執(zhí)行node-addon-api的代碼,node-addon-api對napi的變量進(jìn)行封裝后,才會(huì)調(diào)用Method。所以我們看到Method的入?yún)㈩愋秃蚽api的是不一樣的。最后Method執(zhí)行完返回的時(shí)候,同樣是先回到node-addon-api。node-addon-api把Method的返回值(String對象)轉(zhuǎn)成napi的格式后(napi_value)再返回到napi(這里比較復(fù)雜,目前還沒有深入分析)。
至此我們看到了node-addon-api設(shè)計(jì)的基本思想如圖所示。
大致的思想就是node-addon-api為我們封裝了一層,當(dāng)napi調(diào)用我們定義的內(nèi)容時(shí),會(huì)先經(jīng)過node-addon-api。node-addon-api封裝napi的入?yún)⒑笤僬{(diào)用我們自定義的內(nèi)容。同樣,我們返回內(nèi)容給napi時(shí),也會(huì)經(jīng)過node-addon-api的封裝再回到napi。比如我們在addon里創(chuàng)建一個(gè)數(shù)字時(shí), 我們會(huì)執(zhí)行Number New(napi_env env, double value);New會(huì)調(diào)用napi的napi_create_double創(chuàng)建一個(gè)napi_value變量。接著把napi_value的值封裝到Number,最后返回一個(gè)Number給我們,后續(xù)我們調(diào)用Number的其他方法時(shí),node-addon-api會(huì)從Number對象中拿到保存napi_value的值,再調(diào)用napi的api。這樣我們只需要面對node-addon-api提供的接口而不需要理解napi。另外node-addon-api還做了一些運(yùn)算符重載使得我們寫代碼更容易。比如對Object []的重載。
- Value operator []( const char* utf8name) const;
我們看看實(shí)現(xiàn)。
- 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);
如果大量這樣的代碼將會(huì)非常麻煩和低效。另外node-addon-api對類型進(jìn)行了大量的重載,使得變量的類型轉(zhuǎn)換得以自動(dòng)進(jìn)行不需要強(qiáng)制轉(zhuǎn)換來轉(zhuǎn)換去。比如我們可以直接執(zhí)行以下代碼。
- int32_t num = Number對象;
因?yàn)镹umber對int32_t進(jìn)行了重載。
- 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的實(shí)現(xiàn)原理和思想,實(shí)現(xiàn)的代碼將近萬行,雖然有很多類似的邏輯,但是也有些比較復(fù)雜的封裝,有興趣的同學(xué)可自行閱讀。.