实战篇:开启C++扩展制作之旅——node-addon-api教程
前言
在 Node.js 中,我们可以使用 C++ 编写扩展来为 JavaScript 提供高性能的功能。在编写这些扩展时,我们可以使用多种不同的 API,其中三个常用的是 nan、napi 和 node-addon-api。
1. Nan
Nan 是一个用于编写 Node.js C++ 扩展的工具包。它提供了一组 C++ 模板和宏,用于简化 Node.js 的 C++ Addon API。Nan 的目标是提供一个稳定的 C++ API,这样你就不必每个 Node.js 版本都重新编译你的模块。它支持 Node.js v0.8 到 v14,同时也提供了许多便利的功能,如自动内存管理、V8值的类型转换等等。
2. NAPI
NAPI 是 Node.js 提供的一种稳定的 API,用于编写跨版本的扩展。它的设计目标是提供一个稳定的、面向未来的 API,使得扩展开发者不必担心每个 Node.js 版本的变化。使用 NAPI 编写扩展需要编写较多的代码,但它可以使你的扩展更加稳定并且在不同版本的 Node.js 上运行。
3. Node-addon-api
Node-addon-api 是 Node.js提供的另一个 C++ 扩展 API。它是一个用于编写跨平台的 Node.js C++ 扩展的库。Node-addon-api 是构建在 NAPI 之上的,提供了更加简单的 API,使得扩展开发者可以更加容易地编写跨版本、跨平台的扩展。它还提供了一些方便的功能,如自动内存管理、V8值的类型转换等。
综上,以上三种API都可以用于编写 Node.js 的 C++ 扩展,但它们的设计目标和使用方法略有不同。开发者可以根据自己的需求选择合适的 API。
binding.gyp
binding.gyp
是一个用于描述 C++ 扩展的配置文件,它可以让你指定编译器、编译选项、源文件等等。
binding.gyp
由一个 JSON 对象组成,包含一个或多个 target
。每个 target
描述了一个 C++ 扩展。下面是一些常用的属性:
target_name
: 扩展的名称。sources
: 扩展的源文件。include_dirs
: 头文件的路径。libraries
: 需要链接的库。conditions
: 条件编译。
下面是一个使用node-addon-api编写的模块的 binding.gyp
文件示例:
{
"targets": [
{
"target_name": "myaddon",
"sources": [
"src/myaddon.cc"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!@(node -p \"require('node-addon-api').gyp\")"
]
}
]
}
sources
属性指定了扩展的源文件,include_dirs
属性指定了头文件的路径。需要注意的是,由于使用了node-addon-api,我们可以使用 <!@...>
语法来引用node-addon-api提供的头文件路径和依赖关系。
在上面的示例中,dependencies
属性指定了一个依赖项,它将包含一些额外的编译选项和链接选项。
总之,binding.gyp
是用于描述 C++ 扩展的重要工具,使用node-addon-api编写的模块可以简化其编写过程。开发者可以参考上述示例来编写自己的 binding.gyp
文件。
示例 FileLock
介绍了这么多,我们来看一个实际项目中使用的例子。下面我用c++ 实现了一个 FileLock 用于独占打开文件,其他进程只能只读该文件。
#include <napi.h>
FileLock::~FileLock()
{
#ifdef WIN32
if (this->m_bLocked && this->m_hFileHandle != NULL)
{
CloseHandle(this->m_hFileHandle);
}
this->m_hFileHandle = NULL;
#else
if (this->m_bLocked && this->m_nFd)
{
flock(this->m_nFd, LOCK_UN);
close(this->m_nFd);
}
#endif
this->m_bLocked = false;
}
#ifdef WIN32
std::wstring s2ws(const std::string &s, bool isUtf8 = true)
{
int len;
int slength = (int)s.length() + 1;
len = MultiByteToWideChar(isUtf8 ? CP_UTF8 : CP_ACP, 0, s.c_str(), slength, 0, 0);
std::wstring buf;
buf.resize(len);
MultiByteToWideChar(isUtf8 ? CP_UTF8 : CP_ACP, 0, s.c_str(), slength,
const_cast<wchar_t *>(buf.c_str()), len);
return buf;
}
#endif
Napi::Value FileLock::Lock(const Napi::CallbackInfo &info)
{
#ifdef _WIN32
HANDLE hFile = CreateFileW(s2ws(this->m_sFilePath).c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
int errCode = GetLastError();
Napi::Error::New(info.Env(), "[" + std::to_string(errCode) + "]" + "Failed to open file").ThrowAsJavaScriptException();
return Napi::Boolean::New(info.Env(), false);
}
this->m_hFileHandle = hFile;
this->m_bLocked = true;
return Napi::Boolean::New(info.Env(), true);
#else
int fd = open(this->m_sFilePath.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
int res = flock(fd, LOCK_EX | LOCK_NB);
if (res == -1)
{
Napi::Error::New(info.Env(), "Failed to lock file").ThrowAsJavaScriptException();
return Napi::Boolean::New(info.Env(), false);
}
this->m_bLocked = true;
this->m_nFd = fd;
return Napi::Boolean::New(info.Env(), true);
#endif
return Napi::Boolean::New(info.Env(), false);
}
异步调用 async worker
上面示例在使用中 Lock()
是同步调用的,有的时候我们编写的模块会很耗时,需要异步执行,这个时候就可以使用 Async Worker
。
node-addon-api中的 async worker
是一种在异步线程中执行操作的API。它可以让我们在不阻塞主线程的情况下执行耗时的操作,例如网络请求或长时间运行的计算。
Tips: 在 Electron 应用开发中可能会用到这个,因为 Electron 应用中无法使用 Nodejs 的
worker_threads
来新开一个线程处理复杂事务。
使用async worker
时,需要先创建一个AsyncWorker
对象,并指定Execute
和OnOK
方法。Execute
方法会在异步线程中执行,而OnOK
方法会在执行完成后在主线程中调用。
下面是一个使用async worker
的示例:
#include <napi.h>
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
class MyWorker : public Napi::AsyncWorker {
public:
MyWorker(Napi::Function& callback, int delay)
: Napi::AsyncWorker(callback), delay_(delay) {}
~MyWorker() {}
void Execute() {
// std::this_thread::sleep_for(std::chrono::milliseconds(delay_));
// 处理耗时的任务,不能使用node-addon-api的api,只能用c++
}
void OnOK() {
Napi::HandleScope scope(Env());
Callback().Call({Env().Undefined()});
}
private:
int delay_;
};
Napi::Value Delay(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Function callback = info[1].As<Napi::Function>();
int delay = info[0].As<Napi::Number>().Int32Value();
MyWorker* worker = new MyWorker(callback, delay);
worker->Queue();
return env.Undefined();
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("delay", Napi::Function::New(env, Delay));
return exports;
}
NODE_API_MODULE(addon, Init)
const { delay } = require("bindings")("myaddon");
delay(() => {
// code
}, 2)
在上面的示例中,我们定义了一个MyWorker
类,它继承自AsyncWorker
类,并覆盖了Execute
和OnOK
方法。Execute
方法会在异步线程中执行,而OnOK
方法会在执行完成后在主线程中调用。
总结
综上所述,nan、napi 和 node-addon-api 都是用于编写 Node.js C++ 扩展的API,它们的设计目标和使用方法略有不同。开发者可以根据自己的需求选择合适的API。除此之外,binding.gyp是用于描述 C++ 扩展的重要工具,使用 node-addon-api 编写的模块可以简化其编写过程。最后,Node-addon-api 中的 async worker 是一种在异步线程中执行操作的 API,它可以让我们在不阻塞主线程的情况下执行耗时的操作。开发者可以参考上述示例来编写自己的异步操作。
如果大家对 node-addon-api 感兴趣,后面我会再写一篇详细的实战篇。感谢大家的阅读:)
转载自:https://juejin.cn/post/7205027937714864185