likes
comments
collection
share

实战篇:开启C++扩展制作之旅——node-addon-api教程

作者站长头像
站长
· 阅读数 83

前言

在 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对象,并指定ExecuteOnOK方法。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类,并覆盖了ExecuteOnOK方法。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
评论
请登录