当前位置: 首页 > 工具软件 > Node.native > 使用案例 >

用native C++模块扩展Node.js

韦正业
2023-12-01

Node.js不仅可以加载JavaScript库,还可以使用原生模块(已编译的C / C ++代码)进行扩展。虽然这并不意味着您应该清理现有的JavaScript模块,转而使用良好的C ++语言,但是这些知识可能会在特定用例中派上用场。

我是一名JavaScript开发人员,为什么我会想要混合使用C++?

首先:可以直接访问现有的C / C ++库。而不是像“执行命令”风格那样将这些应用程序称为外部应用程序,直接使用现有的源代码,并以JavaScript运行时可理解的形式将结果传递回Node.js。这样您也可以访问操作系统的低级API

第二:性能。在许多情况下,编写良好的本机代码可能比JavaScript代码更快,性能更高。

Hello, (native) world!

接下来,我们通过一个短小的例子,来准备将一小段c++的代码编译为node的native module。
这里,我们首先会用到Nan(Native Abstractions for Node.js)。它基本上是一个C++头文件,提供了一个命名空间和一组有用的宏,以便与V8 API交互(Node.js中使用的JavaScript引擎)。它也使得你的代码可以面向未来(针对Node.js的下一个版本),因为V8的API在版本之间往往会发生巨大的变化。Nan是Node.js API文档中推荐的解决方案。

首先,我们创建一个.cpp文件(我把它命名为main.cpp),并填入下面的一段代码:

#include <nan.h>

// NAN_METHOD is a Nan macro enabling convenient way of creating native node functions.
// It takes a method's name as a param. By C++ convention, I used the Capital cased name.
NAN_METHOD(Hello) {
    // Create an instance of V8's String type
    auto message = Nan::New("Hello from C++!").ToLocalChecked();
    // 'info' is a macro's "implicit" parameter - it's a bridge object between C++ and JavaScript runtimes
    // You would use info to both extract the parameters passed to a function as well as set the return value.
    info.GetReturnValue().Set(message);
}

// Module initialization logic
NAN_MODULE_INIT(Initialize) {
    // Export the `Hello` function (equivalent to `export function Hello (...)` in JS)
    NAN_EXPORT(target, Hello);
}

// Create the module called "addon" and initialize it with `Initialize` function (created with NAN_MODULE_INIT macro)
NODE_MODULE(addon, Initialize);

请注意,在C++中创建的方法的返回值是void,它们不显式返回任何值。而是在“桥接器” info对象上设置返回值(引用的类型是Nan::FunctionCallbackInfo,“隐式地”在NAN_METHOD宏中传递。
为了运行代码,你仍然需要编译它。不用担心,您不必手动调用C++编译器。Node.js会帮你完成。
为了方便后续的操作,我们通过npm在main.cpp所在目录下创建一个node的项目:

npm init -y

接下来,安装Nan和node-gyp(你已经知道了Nan; node-gyp是一组本地代码编译工具):

npm install nan node-gyp --save

更新你的package.json文件,使其包含以下几行:

{
  "name": "node-native-addons-example",
  "version": "1.0.0",
  "dependencies": {
    "nan": "^2.6.1",
    "node-gyp": "^3.6.0"
  },
  "scripts": {
    "compile": "node-gyp rebuild",
    "start": "node main.js"
  },
  "gypfile": true
}

注意,以下几行是手动添加:

  "scripts": {
    "compile": "node-gyp rebuild",
    "start": "node main.js"
  },
  "gypfile": true

"compile": "node-gyp rebuild" - 用于C ++代码编译
"start": "node main.js" - 为我们的主要可执行脚本

现在创建一个bindings.gyp文件 - 这是node-gyp的一个配置文件。注意它是如何在”sources”下指定源文件”main.cpp”的:

{
  "targets": [
    {
      "include_dirs": [
        "<!(node -e \"require('nan')\")"
      ],
      "target_name": "addon",
      "sources": [ "main.cpp" ]
    }
  ]
}

你还需要在你的机器上安装这三个工具(你在MacOS上应该没问题 - 它们是预安装的;它们也很容易安装在Linux上,例如Ubuntu和apt-get):

  • make
  • g++
  • python 2.7

如果缺少这些信息,node-gyp会报告它,所以不要浪费你的时间来确定你的操作系统上是否有它们。

现在是第一次编译的时候了
npm run compile
(记住,它会启动node-gyp并编译你的C ++源bindings.gyp文件)。如果一切顺利,你应该看到类似的输出:

npm run compile

> temp@1.0.0 compile /Users/caishichao/Applications/temp
> node-gyp rebuild

  CXX(target) Release/obj.target/addon/main.o
  SOLINK_MODULE(target) Release/addon.node
clang: warning: libstdc++ is deprecated; move to libc++ with a minimum deployment target of OS X 10.9 [-Wdeprecated]

很好,你已经编译了你的第一个native module。我们来创建一个JavaScript文件(test.js),您将使用Node.js运行:

// note that the compiled addon is placed under following path
const {Hello} = require('./build/Release/addon');

// `Hello` function returns a string, so we have to console.log it!
console.log(Hello());

并运行:

node test.js
Hello from C++!

这很好,但是更有用的东西呢?

当然。你想知道本地模块真正发光的地方。

为此我们实现一个简单的函数,该函数将检查给定的数字是否是质数,并返回布尔值(true或false)。我们将对函数的C ++和JavaScript版本使用相同的算法,并最终比较相同的相对较大的执行时间。

该算法将:

  • 首先,检查唯一的参数是否是一个数字(如果不是 - 抛出TypeError);
  • 第二,检查数字是否小于2(如果是这样,返回false);
  • 第三,在2到该数字之间的区间中进行迭代,并检查模运算是否为零(如果是的话,打破循环和返回false,因为这个数字不是素数)。
  • 最后,返回true - 数字“通过循环”,所以它必须是素数。

(为了简单起见,我故意忽略对非整数或负数的检查;我将把它作为一个练习给你!)
创建一个isPrime.js文件:

module.exports = (number) => {
  if (typeof number !== 'number') {
    throw new TypeError('argument must be a number!');
  }

  if (number < 2) {
    return false;
  }

  for (let i = 2; i < number; i++) {
    if (number % i === 0) {
      return false;
    }
  }

  return true;
};

现在,对于该main.cpp文件,用下面的代码替换它的内容:

#include <nan.h>

NAN_METHOD(IsPrime) {
    if (!info[0]->IsNumber()) {
        Nan::ThrowTypeError("argument must be a number!");
        return;
    }

    int number = (int) info[0]->NumberValue();

    if (number < 2) {
        info.GetReturnValue().Set(Nan::False());
        return;
    }

    for (int i = 2; i < number; i++) {
        if (number % i == 0) {
            info.GetReturnValue().Set(Nan::False());
            return;
        }
    }

    info.GetReturnValue().Set(Nan::True());
}

NAN_MODULE_INIT(Initialize) {
    NAN_EXPORT(target, IsPrime);
}

NODE_MODULE(addon, Initialize);

打开你的test.js文件,并用下面的代码交换现有的内容:

const {IsPrime} = require('./build/Release/addon'); // native c++
const isPrime = require('./isPrime'); // js

const number = 654188429; // thirty-fifth million first prime number (see https://primes.utm.edu/lists/small/millions/)
const NATIVE = 'native';
const JS = 'js';

console.time(NATIVE);
console.log(`${NATIVE}: checking whether ${number} is prime... ${IsPrime(number)}`);
console.timeEnd(NATIVE);
console.log('');
console.time(JS);
console.log(`${JS}: checking whether ${number} is prime... ${isPrime(number)}`);
console.timeEnd(JS);

重新通过npm run compile编译一下我们的native c++ module,然后运行测试:

node test.js 
native: checking whether 654188429 is prime... true
native: 1845.512ms

js: checking whether 654188429 is prime... true
js: 3177.400ms

可以看到,使用在我的mac上,C ++函数的平均时间为1800ms,JavaScript的时间为3100ms,导致算法的C ++版本比其JavaScript等效的速度快1.7倍。

结论

在这篇文章中,我想告诉你Node.js平台是开放的本地,低级别的扩展。但是,编写这些过程需要一些准备和额外的工具。虽然本机代码确实可以更高性能,但您仍然应该更喜欢JavaScript解决方案,除非迫切需要进行重度优化或者访问低级API。

 类似资料: