JavaScript内核与高级编程之:`Node.js`的`N-API`:如何实现`JavaScript`和`C++`模块的互操作。

各位观众,大家好!我是今天的主讲人,咱们今天聊点刺激的——用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_envnapi_callback_info作为参数,并返回一个napi_value
  • napi_create_string_utf8: 创建一个UTF-8编码的字符串。
  • Init: 这是模块的初始化函数,它接受napi_envexports作为参数。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-apinode-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_errornapi_throw_type_error等等。在C++代码中,你需要检查napi_status的值,如果不是napi_ok,就说明出错了,你需要抛出一个JavaScript异常。

七、 异步操作

N-API也支持异步操作。你可以使用napi_create_async_worknapi_queue_async_worknapi_delete_async_work等函数来创建和管理异步工作。

八、 总结

N-API是一个强大的工具,它可以让你在Node.js中使用C++模块,从而提高性能、访问底层API等等。虽然学习曲线可能稍微陡峭,但一旦掌握了它,你就可以构建更强大、更灵活的Node.js应用程序。

希望今天的讲解能帮助你入门N-API。当然,N-API的内容远不止这些,还有很多高级用法等待你去探索。 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注