一篇了解Node-Addon-Api的設計和實現
本文轉載自微信公眾號「編程雜技」,作者theanarkh 。轉載本文請聯系編程雜技公眾號。
開發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的實現原理和思想,實現的代碼將近萬行,雖然有很多類似的邏輯,但是也有些比較復雜的封裝,有興趣的同學可自行閱讀。.