各位观众,大家好!我是今天的主讲人,咱们今天聊点刺激的——用Node.js的N-API,让JavaScript和C++这两位“老冤家”握手言和,甚至“同居”。
首先,别害怕,虽然听起来高大上,但N-API其实没那么难。想象一下,Node.js就像个精明的生意人,JavaScript是他的母语,但有些脏活累活,或者对性能要求极高的任务,他不得不找C++这位“肌肉男”来帮忙。而N-API,就是他们之间的“翻译官”和“快递员”。
一、 为什么要用N-API?
在没有N-API之前,如果JavaScript想调用C++模块,往往需要借助node-gyp
等工具,构建一个跟特定Node.js版本绑定的addon。这意味着:
- 版本依赖地狱: 每次Node.js升级,你都可能需要重新编译你的C++模块,否则就会出现“水土不服”的情况。
- 学习曲线陡峭: 原来的API(如Nan)比较底层,使用起来比较复杂,容易出错。
- 维护成本高: 为了兼容不同的Node.js版本,你需要维护多个版本的C++模块。
N-API的出现,就是为了解决这些问题。它提供了一个稳定、与Node.js版本无关的ABI(Application Binary Interface),这意味着:
- 一次编译,到处运行: 你的C++模块只需要编译一次,就可以在多个Node.js版本上运行,摆脱了版本依赖的噩梦。
- API更友好: N-API提供了一套更易于使用的API,降低了学习成本。
- 维护成本降低: 你只需要维护一个版本的C++模块,省时省力。
简单来说,N-API就是Node.js官方钦定的“通用语”,让JavaScript和C++之间的交流更加顺畅、高效、可靠。
二、 N-API的核心概念
要理解N-API,需要掌握几个核心概念:
概念 | 解释 |
---|---|
napi_env |
代表Node.js环境,你可以把它想象成JavaScript的世界,所有N-API操作都必须在这个环境中进行。 |
napi_value |
代表JavaScript的值,可以是数字、字符串、对象、数组等等。在C++中,你需要把这些值转换成N-API的napi_value 类型,才能在JavaScript中使用。 |
napi_status |
代表N-API操作的返回值,用于判断操作是否成功。就像C++的errno 一样,你需要检查napi_status 的值,如果不是napi_ok ,就说明出错了。 |
napi_callback_info |
在C++函数被JavaScript调用时,传递给C++函数的信息,包括this 对象、参数等等。 |
三、 一个简单的N-API示例
让我们从一个最简单的例子开始,创建一个C++模块,它导出一个函数,该函数返回一个字符串 "Hello, N-API!"。
1. 创建项目目录
mkdir hello-napi
cd hello-napi
npm init -y # 使用默认配置初始化package.json
2. 创建C++源文件 hello.cc
#include <node_api.h>
napi_value Method(napi_env env, napi_callback_info info) {
napi_status status;
napi_value world;
status = napi_create_string_utf8(env, "Hello, N-API!", NAPI_AUTO_LENGTH, &world);
if (status != napi_ok) return nullptr;
return world;
}
napi_value Init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
代码解释:
#include <node_api.h>
: 引入N-API头文件。Method
: 这是我们的C++函数,它接受napi_env
和napi_callback_info
作为参数,并返回一个napi_value
。napi_create_string_utf8
: 创建一个UTF-8编码的字符串。Init
: 这是模块的初始化函数,它接受napi_env
和exports
作为参数。exports
是一个JavaScript对象,我们可以把C++函数添加到这个对象上,使其可以在JavaScript中使用。napi_create_function
: 创建一个JavaScript函数,将Method
函数包装起来。napi_set_named_property
: 将函数fn
添加到exports
对象上,并命名为hello
。NAPI_MODULE
: 这是一个宏,用于声明N-API模块。NODE_GYP_MODULE_NAME
是一个预定义的宏,用于指定模块的名称。
3. 创建 binding.gyp
文件
这个文件告诉node-gyp
如何编译我们的C++模块。
{
"targets": [
{
"target_name": "hello",
"sources": [ "hello.cc" ],
"include_dirs": [
"<!@(node -p "require('node-addon-api').include")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
代码解释:
target_name
: 指定编译后的模块名称。sources
: 指定C++源文件。include_dirs
: 指定头文件搜索路径。这里使用了node-addon-api
模块来自动获取N-API的头文件路径。defines
: 定义一些编译选项。NAPI_DISABLE_CPP_EXCEPTIONS
禁用C++异常,因为N-API不支持C++异常。
4. 安装 node-addon-api
和 node-gyp
npm install node-addon-api
npm install -g node-gyp
5. 编译C++模块
node-gyp configure
node-gyp build
如果一切顺利,你会在build/Release
目录下找到一个名为hello.node
的文件。这就是我们的C++模块。
6. 创建 JavaScript 文件 index.js
const hello = require('./build/Release/hello');
console.log(hello.hello()); // 输出 "Hello, N-API!"
7. 运行 JavaScript 文件
node index.js
你应该能在控制台上看到 "Hello, N-API!"。
四、 传递参数和返回值
上面只是一个简单的例子,没有涉及参数传递和返回值。现在,让我们创建一个更复杂的例子,它导出一个函数,该函数接受两个数字作为参数,并返回它们的和。
1. 修改 hello.cc
文件
#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, nullptr, nullptr);
if (status != napi_ok) {
napi_throw_type_error(env, nullptr, "Wrong number of arguments");
return nullptr;
}
if (argc < 2) {
napi_throw_type_error(env, nullptr, "Expected two arguments");
return nullptr;
}
napi_valuetype type0;
status = napi_typeof(env, args[0], &type0);
if (status != napi_ok) return nullptr;
napi_valuetype type1;
status = napi_typeof(env, args[1], &type1);
if (status != napi_ok) return nullptr;
if (type0 != napi_number || type1 != napi_number) {
napi_throw_type_error(env, nullptr, "Expected number arguments");
return nullptr;
}
double value0;
status = napi_get_value_double(env, args[0], &value0);
if (status != napi_ok) return nullptr;
double value1;
status = napi_get_value_double(env, args[1], &value1);
if (status != napi_ok) return nullptr;
double sum = value0 + value1;
napi_value num;
status = napi_create_double(env, sum, &num);
if (status != napi_ok) return nullptr;
return num;
}
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;
status = napi_set_named_property(env, exports, "add", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
代码解释:
napi_get_cb_info
: 获取JavaScript传递给C++函数的参数。napi_typeof
: 获取参数的类型。napi_get_value_double
: 将napi_value
转换为double
类型。napi_create_double
: 创建一个double
类型的napi_value
。napi_throw_type_error
: 抛出一个类型错误。
2. 修改 index.js
文件
const hello = require('./build/Release/hello');
console.log(hello.add(1, 2)); // 输出 3
3. 重新编译C++模块并运行JavaScript文件
node-gyp configure
node-gyp build
node index.js
你应该能在控制台上看到 3
。
五、 处理对象和数组
N-API还提供了处理JavaScript对象和数组的API。让我们创建一个例子,它导出一个函数,该函数接受一个JavaScript对象作为参数,并返回一个新的JavaScript对象,其中包含原始对象的所有属性,以及一个名为 sum
的属性,其值为原始对象中所有数字属性的和。
1. 修改 hello.cc
文件
#include <node_api.h>
napi_value ProcessObject(napi_env env, napi_callback_info info) {
napi_status status;
size_t argc = 1;
napi_value args[1];
status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (status != napi_ok) {
napi_throw_type_error(env, nullptr, "Wrong number of arguments");
return nullptr;
}
if (argc < 1) {
napi_throw_type_error(env, nullptr, "Expected one argument");
return nullptr;
}
napi_valuetype type0;
status = napi_typeof(env, args[0], &type0);
if (status != napi_ok) return nullptr;
if (type0 != napi_object) {
napi_throw_type_error(env, nullptr, "Expected an object argument");
return nullptr;
}
napi_value obj = args[0];
// 获取对象的所有属性名
napi_value property_names;
status = napi_get_property_names(env, obj, &property_names);
if (status != napi_ok) return nullptr;
uint32_t num_properties;
status = napi_get_array_length(env, property_names, &num_properties);
if (status != napi_ok) return nullptr;
// 创建一个新的对象
napi_value new_obj;
status = napi_create_object(env, &new_obj);
if (status != napi_ok) return nullptr;
double sum = 0;
// 遍历原始对象的所有属性
for (uint32_t i = 0; i < num_properties; i++) {
napi_value key;
status = napi_get_element(env, property_names, i, &key);
if (status != napi_ok) return nullptr;
// 获取属性名
char property_name[256]; // 假设属性名不会超过256个字符
size_t property_name_length;
status = napi_get_value_string_utf8(env, key, property_name, 256, &property_name_length);
if (status != napi_ok) return nullptr;
// 获取属性值
napi_value value;
status = napi_get_property(env, obj, key, &value);
if (status != napi_ok) return nullptr;
// 将属性添加到新对象
status = napi_set_property(env, new_obj, key, value);
if (status != napi_ok) return nullptr;
// 如果属性值是数字,则将其加到sum中
napi_valuetype value_type;
status = napi_typeof(env, value, &value_type);
if (status != napi_ok) return nullptr;
if (value_type == napi_number) {
double num_value;
status = napi_get_value_double(env, value, &num_value);
if (status != napi_ok) return nullptr;
sum += num_value;
}
}
// 创建一个名为 "sum" 的属性,并将其值设置为 sum
napi_value sum_value;
status = napi_create_double(env, sum, &sum_value);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, new_obj, "sum", sum_value);
if (status != napi_ok) return nullptr;
return new_obj;
}
napi_value Init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, ProcessObject, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "processObject", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
代码解释:
napi_get_property_names
: 获取对象的所有属性名。napi_get_array_length
: 获取数组的长度。napi_get_element
: 获取数组中的元素。napi_get_value_string_utf8
: 将napi_value
转换为UTF-8字符串。napi_get_property
: 获取对象的属性值。napi_set_property
: 设置对象的属性值。napi_create_object
: 创建一个新的JavaScript对象。
2. 修改 index.js
文件
const hello = require('./build/Release/hello');
const obj = {
a: 1,
b: 2,
c: "hello",
d: 4
};
const result = hello.processObject(obj);
console.log(result); // 输出 { a: 1, b: 2, c: 'hello', d: 4, sum: 7 }
3. 重新编译C++模块并运行JavaScript文件
node-gyp configure
node-gyp build
node index.js
你应该能在控制台上看到 { a: 1, b: 2, c: 'hello', d: 4, sum: 7 }
。
六、 错误处理
N-API提供了一些函数来处理错误,例如napi_throw_error
、napi_throw_type_error
等等。在C++代码中,你需要检查napi_status
的值,如果不是napi_ok
,就说明出错了,你需要抛出一个JavaScript异常。
七、 异步操作
N-API也支持异步操作。你可以使用napi_create_async_work
、napi_queue_async_work
、napi_delete_async_work
等函数来创建和管理异步工作。
八、 总结
N-API是一个强大的工具,它可以让你在Node.js中使用C++模块,从而提高性能、访问底层API等等。虽然学习曲线可能稍微陡峭,但一旦掌握了它,你就可以构建更强大、更灵活的Node.js应用程序。
希望今天的讲解能帮助你入门N-API。当然,N-API的内容远不止这些,还有很多高级用法等待你去探索。 咱们下期再见!