1_q9myzo5Au8OfsaSrCodNmw5555.png

node-api简述

在没有出现node-api的时候。我们编写的node原生扩展使用的是v8的提供的api。随着javaScript标准的变化和v8版本升级不可避免地对api进行破坏式升级。虽然v8对外提供的api只是对v8内部的api的一个代理(例如v8::Isolate只是对v8::internal::Isolate的代理),但v8也不保证对外提供的api的稳定性。这样就会导致一个问题。当我们使用不同版本的v8进行原生扩展的开发,随着node使用v8版本的升级。会出现一种情况就是编写的原生扩展需要为不同的node版本提供不同的版本。因为他们他们是使用的v8的ABI(应用程序二进制接口)不一致。为了解决这个问题。出现了node-api。
node-api 是用于构建原生插件的 API。它独立于底层 JavaScript 运行时(例如 V8)并作为 Node.js 本身的一部分进行维护。此 API 将在 Node.js 的各个版本中保持稳定的应用程序二进制接口 (ABI)。它旨在将插件与底层 JavaScript 引擎中的更改隔离开来,并允许为一个版本编译的模块无需重新编译即可在更高版本的 Node.js 上运行。插件是使用本文档中概述的相同方法/工具(node-gyp 等)构建/打包的。唯一的区别是本机代码使用的 API 集。使用node-api 中可用的函数,而不是使用 V8 或Node.js API 的本机抽象。创建和维护受益于 node-api 提供的 ABI 稳定性的插件会带来某些 实现方面的考虑
node_api实际上只是在v8的基础上增加了一层代理,代理层通过数据的互相转换从而达到可以对外统一api。即使未来v8的api有变化。只要node-api对外保持不变即可。这就是node-api的实现原理。
image.png
得益于node-api的抽象能力。甚至可以实现不使用v8作为node唯一的javaScript引擎。假设有一天。node可以全部使用node-api进行开发。使用者可以随意切换javaScript引擎,例如quick.js等。

node-api的使用

node-api 公开的 API 通常用于创建和操作 JavaScript 值。概念和操作通常映射到 ECMA-262 语言规范中指定的想法。API 具有以下属性:

  • 所有 node-api调用都返回类型为 的状态代码napi_status。此状态指示 API 调用是成功还是失败。
  • 由于返回值由node-api 占了位置。所以node-api 的返回值通过 out 参数传递。
  • 所有 JavaScript 值都被抽象为一个名为 的不透明类型 napi_value
  • 如果出现错误状态代码,可以使用 获取其他信息napi_get_last_error_info

node-api 是一种 C API,可确保跨 Node.js 版本和不同编译器级别的 ABI 稳定性。C++ API 可以更容易使用。为了支持使用 C++,该项目维护了一个名为node-addon-api. 这个包装器提供了一个可内联的 C++ API。构建的二进制文件node-addon-api将取决于 Node.js 导出的基于 node-api C 的函数的符号。node-addon-api是编写调用 node-api 的代码的更有效方法。
下面看一下v8的api和node-api编写node原生扩展进行对比。看看有什么不同。

v8版本

  1. #include <node.h>
  2. void add(const v8::FunctionCallbackInfo<v8::Value>& args) {
  3. v8::Isolate* isolate = args.GetIsolate();
  4. v8::Local<v8::Context> context = isolate->GetCurrentContext();
  5. int first = args[0]->ToInt32(context).ToLocalChecked()->Int32Value(context).FromJust();
  6. int second = args[1]->ToInt32(context).ToLocalChecked()->Int32Value(context).FromJust();
  7. args.GetReturnValue().Set(first + second);
  8. }
  9. void Initialize(v8::Local<v8::Object> exports,v8::Local<v8::Value> module,v8::Local<v8::Context> context) {
  10. v8::Isolate *isolate = context->GetIsolate();
  11. // 设置add函数
  12. exports->Set(context, v8::String::NewFromUtf8Literal(isolate, "add"), v8::Function::New(context, add).ToLocalChecked()).FromJust();
  13. // 设置属性result = 1
  14. exports->Set(context, v8::String::NewFromUtf8Literal(isolate, "result"), v8::Number::New(isolate, 1)).FromJust();
  15. }
  16. // 模块导出
  17. NODE_MODULE(utils, Initialize)

node-api版本

#include <node.h>
#include <node_api.h>

napi_value add(napi_env env, napi_callback_info info) {

    napi_status status;

    size_t argc = 2;
    napi_value args[2];

    // 获取参数
    status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
    if (status != napi_ok) {
        return nullptr;
    }

    int first;
    status = napi_get_value_int32(env, args[0], &first);
    if (status != napi_ok) {
        return nullptr;
    }
    int second;
    status = napi_get_value_int32(env, args[1], &second);
    if (status != napi_ok) {
        return nullptr;
    }
    napi_value result;
    // 计算返回结果
    status = napi_create_int32(env, first + second, &result);
    if (status != napi_ok) {
        return nullptr;
    }
    return result;
}

napi_value init(napi_env env, napi_value exports) {
    napi_status status;

    napi_value fn;
    status = napi_create_function(env, nullptr, 0, add, nullptr, &fn);
    if (status != napi_ok) {
        return nullptr;
    }
    // 设置函数add 
    status = napi_set_named_property(env, exports, "add", fn);
    if (status != napi_ok) {
        return nullptr;
    }

    napi_value result;
    status = napi_create_int32(env, 1, &result);
    if (status != napi_ok) {
        return nullptr;
    }
    // 设置变量result = 1;
    status = napi_set_named_property(env, exports, "result", result);
    if (status != napi_ok) {
        return nullptr;
    }

    return exports;
}
// 模块导出
NAPI_MODULE(utils, init)

可以看到。node-api相对v8提供的api实现起来比较啰嗦。因为每个操作函数的返回值作为函数调用是否成功的标记。所以,返回结果需要通过传递参数的指针从而达到返回结果的目的。node-api把创建javaScript类型的数据全局封转成独立的函数napi_create_*。操作起来略显麻烦。
还有一个不同的是,注册宏不同。node-api使用了NAPI_MODUL宏进行了模块的进行了注册。我们看一下NAPI_MODUL的实现。

extern "C" {  
    static napi_module _module =                                      
        {                                                                 
          1,                                            
          0,                                                         
          "main.cpp",                                                       
          init,                                                        
          "utils",                                                       
          NULL,                                                           
          {0},                                                            
        };

     static void __cdecl _register_utils(void);                             
       __declspec(dllexport, allocate(".CRT$XCU"))                         
      void (__cdecl* _register_utils_)(void) = _register_utils;                              
       static void __cdecl _register_utils(void) {
         napi_module_register(&_module);  
     } 
}
// napi_module的声明
typedef struct napi_module {
  int nm_version; // 版本号,现阶段都是1
  unsigned int nm_flags; // 模块标记
  const char* nm_filename; // 模块文件名
  napi_addon_register_func nm_register_func; // 注册函数
  const char* nm_modname; // 模块名称
  void* nm_priv;
  void* reserved[4]; //预留指针
} napi_module;

napi_module结构体和node_module 结构类似。napi_module的版本属性 nm_version为1。这是内部通过该标识去判断模块是不是node-api编写原生扩展。从而调用不同的注册函数进行模块注册。
image.png
node-api也是采用了自动注册的方式进行模块的注册。最后调用了api_module_register执行模块的注册。

void napi_module_register(napi_module* mod) {
  // 把napi_module的结构体转成node_module结构体
  node::node_module* nm = new node::node_module(node::napi_module_to_node_module(mod));
  // 和其他原生模块一样最终还是调用了node::node_module_register进行模块的注册
  node::node_module_register(nm);
}

node-api原理

node-api函数分类 (1).png
node-api定义了一层操作javaScript和与javaScript数据双向交互的抽象接口js_native_api.h查看更多的api。只要都是通过c语言类型创建node-api类型的javaScript数据类型。或者通过node-api的javaScript类型转换成c语言类型的函数。napi_env 对象作为node-api的上下文对象。每个操作函数中的第一个参数都是它。在node-api定义的抽象接口文件中,napi_env为一个指向空的结构体指针。实现文件可以重新定义该结构体。

// 这里只列举了字符串
// 创建node-api类型的javaScript字符串
napi_status napi_create_string_utf8(napi_env env,  const char* str, size_t length,     napi_value* result);
// 将node-api的javaScript字符串转换成c语言类型的字符串。
napi_get_value_string_utf8(napi_env env,napi_value value,   char* buf, size_t bufsize,size_t* result);

node-api定义了node-api类型的javaScript类型和抽象数据类型。js_native_api_types.h

// 实现类可以重新定义结构体的数据模型。
// node-api的上下文对象。(不是javaScript的上下文。在node-api的v8实现中,它包含了v8::Isolate, v8::Context等)
typedef struct napi_env__* napi_env;
// 代表着node-api的javaScript值。
typedef struct napi_value__* napi_value;

typedef struct napi_ref__* napi_ref;
typedef struct napi_handle_scope__* napi_handle_scope;
typedef struct napi_escapable_handle_scope__* napi_escapable_handle_scope;
typedef struct napi_callback_info__* napi_callback_info;
typedef struct napi_deferred__* napi_deferred;

// node-api定义javaScript类型
typedef enum {
  napi_undefined,
  napi_null,
  napi_boolean,
  napi_number,
  napi_string,
  napi_symbol,
  napi_object,
  napi_function,
  napi_external,
  napi_bigint,
} napi_valuetype;

在内部,有一个使用v8的实现node-api的实现文件js_native_api_v8.cc。在讲解实现类型前,我们先看一下下面node-api几个重要的数据类型。

  1. api_status:指示 node-api 调用成功或失败的完整状态代码。是一个枚举值。每个操作函数最后的返回值都为npi_status。除了napi_ok外,其他放回都是代表着函数的执行失败。在v8的的node-api实现中,可以通过napi_env对象的napi_extended_error_info last_error属性获取异常。

    typedef enum {
    napi_ok,
    napi_invalid_arg,
    napi_object_expected,
    napi_string_expected,
    napi_name_expected,
    napi_function_expected,
    napi_number_expected,
    napi_boolean_expected,
    napi_array_expected,
    napi_generic_failure,
    napi_pending_exception,
    napi_cancelled,
    napi_escape_called_twice,
    napi_handle_scope_mismatch,
    napi_callback_scope_mismatch,
    napi_queue_full,
    napi_closing,
    napi_bigint_expected,
    napi_date_expected,
    napi_arraybuffer_expected,
    napi_detachable_arraybuffer_expected,
    napi_would_deadlock,  /* 还有被使用/
    } napi_status;
    
  2. napi_env: napi_env用于表示底层node-api 实现可用于保持 VM 特定状态的上下文。此结构在调用时传递给本机函数,并且必须在进行 node-api 调用时传回。具体来说,napi_env必须将调用初始本机函数时传入的相同内容传递给任何后续嵌套的 node-api 调用。在node-api中,napi_env是一个抽象结构体、在v8的实现中,对改类型实现了从新的定义。

  3. napi_value: 这是一个用于表示 JavaScript 值的不透明指针。在v8的实现中对应着v8的对象指针。下面是v8实现的v8类型和node-api类型的相互转换的函数。 ```cpp // 执行空结构体指针 typedef struct napivalue_* napi_value; static_assert(sizeof(v8::Local) == sizeof(napi_value), “Cannot convert between v8::Local and napi_value”);

// v8数据类型转node-api类型 inline napi_value JsValueFromV8LocalValue(v8::Local local) { return reinterpret_cast(local); } // node-api转v8类型 inline v8::Local V8LocalValueFromJsValue(napi_value v) { v8::Local local; memcpy(static_cast<void>(&local), &v, sizeof(v)); return local; }

node-api插件使用了`node-api`的时候。实际上只是通过node维护的一个稳定的abi接口代理层node-api。node-api最终还是通过调用javaScript引擎执行具体的逻辑。我们看一下node-api在node的实现v8实现。`js_native_api_v8.cc`
```cpp

napi_status napi_create_string_utf8(napi_env env,
                                    const char* str,
                                    size_t length,
                                    napi_value* result) {
  // 数据校验  
  RETURN_STATUS_IF_FALSE(env,
      (length == NAPI_AUTO_LENGTH) || length <= INT_MAX,
      napi_invalid_arg);

  auto isolate = env->isolate;
  auto str_maybe = v8::String::NewFromUtf8(isolate,
                              str,
                              v8::NewStringType::kNormal,
                              static_cast<int>(length));
  // 校验句柄是否为空
  CHECK_MAYBE_EMPTY(env, str_maybe, napi_generic_failure);
  // 把v8类型转换成node-api类型。
  *result = v8impl::JsValueFromV8LocalValue(str_maybe.ToLocalChecked());
  // 记录最终状态
  return napi_clear_last_error(env);
}

在v8的实现中,重新定义了napi_env__ 结构体。所以napi_env 对象使用的是v8实现的napi_env__结构体。v8实现的napi_env__结构体中存储了v8执行过程中所需的重要数据类型。v8::Isolate等。

struct napi_env__ {
  explicit napi_env__(v8::Local<v8::Context> context)
      : isolate(context->GetIsolate()),
        context_persistent(isolate, context) {
    CHECK_EQ(isolate, context->GetIsolate());
  }
  virtual ~napi_env__() {
    v8impl::RefTracker::FinalizeAll(&finalizing_reflist);
    v8impl::RefTracker::FinalizeAll(&reflist);
  }
  v8::Isolate* const isolate;
  v8impl::Persistent<v8::Context> context_persistent;

  inline v8::Local<v8::Context> context() const {
    return v8impl::PersistentToLocal::Strong(context_persistent);
  }

  inline void Ref() { refs++; }
  inline void Unref() { if ( --refs == 0) delete this; }

  virtual bool can_call_into_js() const { return true; }
  virtual v8::Maybe<bool> mark_arraybuffer_as_untransferable(
      v8::Local<v8::ArrayBuffer> ab) const {
    return v8::Just(true);
  }

  static inline void
  HandleThrow(napi_env env, v8::Local<v8::Value> value) {
    env->isolate->ThrowException(value);
  }

  template <typename T, typename U = decltype(HandleThrow)>
  inline void CallIntoModule(T&& call, U&& handle_exception = HandleThrow) {
    int open_handle_scopes_before = open_handle_scopes;
    int open_callback_scopes_before = open_callback_scopes;
    napi_clear_last_error(this);
    call(this);
    CHECK_EQ(open_handle_scopes, open_handle_scopes_before);
    CHECK_EQ(open_callback_scopes, open_callback_scopes_before);
    if (!last_exception.IsEmpty()) {
      handle_exception(this, last_exception.Get(this->isolate));
      last_exception.Reset();
    }
  }

  virtual void CallFinalizer(napi_finalize cb, void* data, void* hint) {
    v8::HandleScope handle_scope(isolate);
    CallIntoModule([&](napi_env env) {
      cb(env, data, hint);
    });
  }

  v8impl::Persistent<v8::Value> last_exception;
  v8impl::RefTracker::RefList reflist;
  v8impl::RefTracker::RefList finalizing_reflist;
  napi_extended_error_info last_error;
  int open_handle_scopes = 0;
  int open_callback_scopes = 0;
  int refs = 1;
  void* instance_data = nullptr;
};

每个操作函数,都是通过把运行结果记录在napi_env 对象上。在上面的函数最后调用了napi_clear_last_error()

// 在env的last_error结构体对象上设置错误信息。
static inline napi_status napi_clear_last_error(napi_env env) {
  env->last_error.error_code = napi_ok;
  env->last_error.engine_error_code = 0;
  env->last_error.engine_reserved = nullptr;
  return napi_ok;
}

typedef struct {
  const char* error_message;
  void* engine_reserved;
  uint32_t engine_error_code;
  napi_status error_code;
} napi_extended_error_info;

再看一个实现。node-api实现类型判断。在使用v8中实现。可以看到通过借助了v8大量的类型判断实现了napi的类型判断。

napi_status napi_typeof(napi_env env,
                        napi_value value,
                        napi_valuetype* result) {
  // 把node-api类型转换成v8类型
  v8::Local<v8::Value> v = v8impl::V8LocalValueFromJsValue(value);
  if (v->IsNumber()) {
    *result = napi_number;
  } else if (v->IsBigInt()) {
    *result = napi_bigint;
  } else if (v->IsString()) {
    *result = napi_string;
  } else if (v->IsFunction()) {
    *result = napi_function;
  } else if (v->IsExternal()) {
    *result = napi_external;
  } else if (v->IsObject()) {
    *result = napi_object;
  } else if (v->IsBoolean()) {
    *result = napi_boolean;
  } else if (v->IsUndefined()) {
    *result = napi_undefined;
  } else if (v->IsSymbol()) {
    *result = napi_symbol;
  } else if (v->IsNull()) {
    *result = napi_null;
  } else {
    return napi_set_last_error(env, napi_invalid_arg);
  }

  return napi_clear_last_error(env);
}

所以,终上所述。在node-api的设计上。使用了抽象公共数据。默认是一个指向空结构体的指针。

typedef struct napi_env__* napi_env;
typedef struct napi_value__* napi_value;
typedef struct napi_ref__* napi_ref;
typedef struct napi_handle_scope__* napi_handle_scope;
typedef struct napi_escapable_handle_scope__* napi_escapable_handle_scope;
typedef struct napi_callback_info__* napi_callback_info;
typedef struct napi_deferred__* napi_deferred;

不同的node-api实现可以从新定义上面的抽象结构体。在使用v8的实现中, 重新实现了napi_env结构体。把大量v8运行时需要的实例属性保存在该结构体中,例如v8::Isolate,v8::Context等,在任何的操作函数中通过传递napi_env 结构体指针,从而达到可以在不同的函数中可以获取自定义的数据类型。对于剩下的类型,v8实现中并没有对数据进行实现。直接使用了原来的数据类型。通过定义空结构体实现数据抽象,这也是在c语言中可以实现抽象能力的办法,libuv也是通过c语言的一些技巧实现结构体的继承关系(公共数据)。

node-addon-api

node-addon-api数据类型关系 (1).pngnode-addon-api并不属于node去开发维护,由于node-api使用c语言开发,编写的node-api的node原生扩展显得有点啰嗦。node-addon-api居于node-api的c++包装,把node-api的函数调用使用了c++类去做了包装。在上面图片中我们可以看到。node-addon-api对数据类型的封装和v8提供的接口类似。下面我们拿Napi::Number源码做解析。先看一下如何创建一个node-addon-api类型的javaScript数字。

    Napi::Number number = Napi::Number::New(env, 1);
    int result = number.Int32Value();

先看一下Napi::Number类定义。

// 内容有省略,只列举部分函数和属性
class Number : public Value {
    public:
    static Number New(napi_env env,   double value);

    Number();
    Number(napi_env env,
           napi_value value);
    int32_t Int32Value()  const; 
    uint32_t Uint32Value() const;
    int64_t Int64Value()  const;
    float FloatValue()   const; 
};
 class Value {
  public:
    Value();                          
    Value(napi_env env,
          napi_value value);
    template <typename T>
    static Value From(napi_env env, const T& value);
    operator napi_value() const;
    // 不同的类型进行比较
    bool operator ==(const Value& other) const;
    bool operator !=(const Value& other) const;
    bool StrictEquals(const Value& other) const;
    Napi::Env Env() const;
    bool IsEmpty() const;
    // 获取node-api的javaScipt类型
    napi_valuetype Type() const;
    // 类型判断
    bool IsUndefined() const;   
    bool IsNull() const;     
    template <typename T> T As() const;
    // 类型转换
    Boolean ToBoolean() const;
    Number ToNumber() const;  
    String ToString() const;  
  protected:
    napi_env _env;
    napi_value _value;
  };

创建Napi::Number::New实现。可以看到,先通过node-api函数napi_create_double创建node-api的javaScript数字类型。然后再通过调用Napi::Number的构造函数。构造函数也只是 把 node-api的javaScript值传递到基类Napi::Value保存。

inline Number Number::New(napi_env env, double val) {
  napi_value value;
  napi_status status = napi_create_double(env, val, &value);
  if (status != napi_ok) {                             
    Napi::Error::New(env).ThrowAsJavaScriptException();  
    return __VA_ARGS__;                                  
  }
  return Number(env, value);
}
Number::Number(napi_env env, napi_value value) : Value(env, value) {}

接着我们查看从node-addon-api类型装换成c类型的整数类型。可以看到。通把基类Napi::Value保存了创建的值,在调用Napi::Number实例函数Int32Value()又通过了node-apiapi_get_value_int32 转换成c整型类型。

inline int32_t Number::Int32Value() const {
  int32_t result;
  napi_status status = napi_get_value_int32(_env, _value, &result);
   if (status != napi_ok) {                             
    Napi::Error::New(env).ThrowAsJavaScriptException();  
    return __VA_ARGS__;                                  
  }
  return result;
}

可以看到,node-addon-api只是把node-api有关类型的操作函数包装成一个c++的类。例如把操作字符串的函数包装在Napi::String类里面。通过继承关系。把操作的类型全部存放在Napi::Value里面,子类只负责做类型的转换操作。
下面线看一下node-addon-api开发的node原生扩展。


#include <napi.h>

Napi::Number add(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    int first = info[0].As<Napi::Number>().Int32Value();
    int second = info[1].As<Napi::Number>().Int32Value();
    return Napi::Number::New(env, first + second);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    // 创建add 函数。
    exports.Set(Napi::String::New(env, "add"),Napi::Function::New(env, add));
    // 创建 result = 1;
    exports.Set(Napi::String::New(env, "result"), Napi::Number::New(env,1));
    return exports;
}

NODE_API_MODULE(utils, Init)

node-addon-api封装的风格和v8提供的api使用类似。最后在后面调用了NODE_API_MODULE()进行了模块的注册工作。

static napi_value __napi_utils(napi_env env, napi_value exports) {       
   return Napi::RegisterModule(env, exports,Init);                        
}

extern "C" {  
    static napi_module _module =                                      
        {                                                                 
          1,                                            
          0,                                                         
          "main.cpp",                                                       
          __napi_init,                                                        
          "utils",                                                       
          NULL,                                                           
          {0},                                                            
        };

     static void __cdecl _register_utils(void);                             
       __declspec(dllexport, allocate(".CRT$XCU"))                         
      void (__cdecl* _register_utils_)(void) = _register_utils;                              
       static void __cdecl _register_utils(void) {
          napi_module_register(&_module);                 
     } 
}

可以看到。__napi_utils作为模块注册函数。再执行Napi::RegisterModule。最终调Init()注册函数。并把结果转换成node-api数据类型。

inline napi_value RegisterModule(napi_env env,
                                 napi_value exports,
                                 ModuleRegisterCallback registerCallback) {
  return details::WrapCallback([&] {
    return napi_value(registerCallback(Napi::Env(env),
                                       Napi::Object(env, exports)));
  });
}

node-api的插件加载

node的原生扩展章节中我们知道, 当加载的是node-api的时候。使用了napi_module_register_by_symbol()执行模块的注册工作。

oid napi_module_register_by_symbol(v8::Local<v8::Object> exports,
                                    v8::Local<v8::Value> module,
                                    v8::Local<v8::Context> context,
                                    napi_addon_register_func init) {
  node::Environment* node_env = node::Environment::GetCurrent(context);
  std::string module_filename = "";
  if (init == nullptr) {
    CHECK_NOT_NULL(node_env);
    node_env->ThrowError(
        "Module has no declared entry point.");
    return;
  }

  v8::Local<v8::Value> filename_js;
  v8::Local<v8::Object> modobj;
  if (module->ToObject(context).ToLocal(&modobj) &&
      modobj->Get(context, node_env->filename_string()).ToLocal(&filename_js) &&
      filename_js->IsString()) {
    node::Utf8Value filename(node_env->isolate(), filename_js);  // Cast

    module_filename = std::string("file://") + (*filename);
  }

  // 创建一个 napi_env的实现类。
  napi_env env = v8impl::NewEnv(context, module_filename);

  napi_value _exports;

  env->CallIntoModule([&](napi_env env) {
    _exports = init(env, v8impl::JsValueFromV8LocalValue(exports));
  });

  if (_exports != nullptr &&
      _exports != v8impl::JsValueFromV8LocalValue(exports)) {
    napi_value _module = v8impl::JsValueFromV8LocalValue(module);
    napi_set_named_property(env, _module, "exports", _exports);
  }
}