node-addon-api 的错误处理

image

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
// napi exapmle

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
// node-addon-api exapmle

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
// napi-inl.h

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
    // napi-inl.h

    #ifdef NAPI_CPP_EXCEPTIONS

    #define NAPI_THROW_IF_FAILED(env, status, ...) \
    if ((status) != napi_ok) throw Napi::Error::New(env);

    #else // NAPI_CPP_EXCEPTIONS

    #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) { } // 转换构造函数(C++11)
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; // OK:复制初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A::operator bool()
bool na1 = a1; // OK:复制初始化选择 A::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化

// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
B b5 = (B)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}

小结

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',
},
}
]
}