napi 与 node-addon-api
使用 napi 时经常要通过 napi_xxx 函数的返回值去判断一下本次操作是否成功, 于是就需要写大量的 assert 断言去保证程序始终是按预期之内运行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
static napi_value CreateObject(napi_env env, const napi_callback_info info) { napi_status status;
size_t argc = 1; napi_value args[1]; status = napi_get_cb_info(env, info, &argc, args, NULL, NULL); assert(status == napi_ok);
napi_value obj; status = napi_create_object(env, &obj); assert(status == napi_ok);
status = napi_set_named_property(env, obj, "msg", args[0]); assert(status == napi_ok);
return obj; }
|
而使用 node-addon-api 实现同样的功能的代码则比较简洁
1 2 3 4 5 6 7 8 9
|
Napi::Object CreateObject(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Napi::Object obj = Napi::Object::New(env); obj.Set(Napi::String::New(env, "msg"), info[0].ToString());
return obj; }
|
NAPI_THROW_IF_FAILED
node-addon-api 其实是在每个操作函数中都内置了错误处理, 如下面代码的 NAPI_THROW_IF_FAILED 宏就是完成错误处理的任务
1 2 3 4 5 6 7 8
|
inline Object Object::New(napi_env env) { napi_value value; napi_status status = napi_create_object(env, &value); NAPI_THROW_IF_FAILED(env, status, Object()); return Object(env, value); }
|
而 NAPI_THROW_IF_FAILED 宏定义又有两种表现形式, 根据是否开启了 NAPI_CPP_EXCEPTIONS 来决定当前抛出的是一个 C++ 层面的错误还是 Js 层面的错误
- C++ 错误 > throw Napi::Error::New(env): 如果 C++ 代码外层没有 try catch 则程序直接退出
- Js 错误 > Napi::Error::New(env).ThrowAsJavaScriptException: C++ 代码仍然往下运行, 如果 Js 代码外层没有 try catch 则程序直接退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
#ifdef NAPI_CPP_EXCEPTIONS
#define NAPI_THROW_IF_FAILED(env, status, ...) \ if ((status) != napi_ok) throw Napi::Error::New(env);
#else
#define NAPI_THROW_IF_FAILED(env, status, ...) \ if ((status) != napi_ok) { \ Napi::Error::New(env).ThrowAsJavaScriptException(); \ return __VA_ARGS__; \ }
|
MaybeOrValue
既然抛出的 Js 错误不会终止 C++ 程序运行, 那么后面运行的 C++ 代码如果更好的判断上一次操作是否正确返回了值。于是 MaybeOrValue 类承担了这个精准而又不失优雅的任务, MaybeOrValue 类在 Node 以及 v8 代码中也是比较常见的
如下面的 Value().Get 操作, 如果 napi_get_named_property 函数操作失败, 就会出现预期外的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| inline MaybeOrValue<Napi::Value> ObjectReference::Get( const char* utf8name) const { EscapableHandleScope scope(_env); MaybeOrValue<Napi::Value> result = Value().Get(utf8name); #ifdef NODE_ADDON_API_ENABLE_MAYBE if (result.IsJust()) { return Just(scope.Escape(result.Unwrap())); } return result; #else if (scope.Env().IsExceptionPending()) { return Value(); } return scope.Escape(result); #endif }
inline MaybeOrValue<Value> Object::Get(const char* utf8name) const { napi_value result; napi_status status = napi_get_named_property(_env, _value, utf8name, &result); NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); }
|
经过进一步的展开宏定义的实现, 发现如果操作成功运行的是 Napi::Just, 否则是 Napi::Nothing
1 2 3 4 5 6 7 8 9 10 11 12
| #define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ return Napi::Just<type>(result); #define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ NAPI_THROW_IF_FAILED(env, status, Napi::Nothing<type>()) #define NAPI_THROW_IF_FAILED(env, status, ...) \ if ((status) != napi_ok) { \ Napi::Error::New(env).ThrowAsJavaScriptException(); \ return __VA_ARGS__; \ }
|
- Just 构造的是一个有返回值的 MaybeOrValue 类
- Nothing 构造的是一个没有返回值的 MaybeOrValue 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template <class T> inline Maybe<T> Nothing() { return Maybe<T>(); }
template <class T> inline Maybe<T> Just(const T& t) { return Maybe<T>(t); }
template <class T> Maybe<T>::Maybe() : _has_value(false) {}
template <class T> Maybe<T>::Maybe(const T& t) : _has_value(true), _value(t) {}
|
MaybeOrValue 类则提供了如下的实用属性来表示当前的状态
- IsNothing 函数表示调用失败的没有返回值的情况
- IsJust 函数则表示调用成功的情况
- Unwrap 函数则可以把调用的返回值给返回出去
- Check 函数则会去进行一个断言检查, 当没有值时就会抛错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| template <class T> bool Maybe<T>::IsNothing() const { return !_has_value; }
template <class T> bool Maybe<T>::IsJust() const { return _has_value; }
template <class T> void Maybe<T>::Check() const { NAPI_CHECK(IsJust(), "Napi::Maybe::Check", "Maybe value is Nothing."); }
template <class T> T Maybe<T>::Unwrap() const { NAPI_CHECK(IsJust(), "Napi::Maybe::Unwrap", "Maybe value is Nothing."); return _value; }
|
NODE_ADDON_API_ENABLE_MAYBE
这里说一个题外话, 一开始看叉了, 发现返回值不是 MaybeOrValue 类, 而是 napi_xxx 函数返回的原始值。其实是因为会根据是否开启了 NODE_ADDON_API_ENABLE_MAYBE 来决定返回值是否经过 MaybeOrValue 包裹, 而 vscode 跳转则定位到了返回的原始值的宏定义处
1 2 3 4 5 6 7 8 9 10 11 12 13
| #ifdef NODE_ADDON_API_ENABLE_MAYBE #define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ NAPI_THROW_IF_FAILED(env, status, Napi::Nothing<type>())
#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ return Napi::Just<type>(result); #else
#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ return result; #endif
|
当时想了很久没有整明白 Value().Get(utf8name) 返回的明明不是 MaybeOrValue 类型, 难道是 MaybeOrValue 类的什么 operator 接口触发了类型转换 ?
1
| MaybeOrValue<Napi::Value> result = Value().Get(utf8name);
|
随着对上面疑惑的探索, 如下的代码中的 A a1 = 1 代码中的 1 不是 A 类型也能赋值成功是因为触发了 C++ 的隐性转换, 这里的 1 其实就被当成了构造函数的参数了
而构造函数加了 explicit 关键字的 B 类则不能进行上面的隐性转换, 不过还是能通过 static_cast 等进行显性转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| struct A { A(int) { } A(int, int) { } operator bool() const { return true; } }; struct B { explicit B(int) { } explicit B(int, int) { } explicit operator bool() const { return true; } }; int main() { A a1 = 1; A a2(2); A a3 {4, 5}; A a4 = {4, 5}; A a5 = (A)1; if (a1) ; bool na1 = a1; bool na2 = static_cast<bool>(a1);
B b2(2); B b3 {4, 5};
B b5 = (B)1; if (b2) ;
bool nb2 = static_cast<bool>(b2); }
|
小结
binding.gyp 文件中可通过如下配置开启 NAPI_CPP_EXCEPTIONS 与 NODE_ADDON_API_ENABLE_MAYBE 特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "targets": [ { "target_name": "addon", "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], "sources": [ "addon.cc" ], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], 'defines': [ 'NAPI_CPP_EXCEPTIONS', 'NODE_ADDON_API_ENABLE_MAYBE' ], 'msvs_settings': { 'VCCLCompilerTool': { 'ExceptionHandling': 1, 'EnablePREfast': 'true', }, }, 'xcode_settings': { 'CLANG_CXX_LIBRARY': 'libc++', 'MACOSX_DEPLOYMENT_TARGET': '10.7', 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', }, } ] }
|