Node.js通用计算10-- Node.js调用c/c++(简介、工具)

研究了大半天 Nodejs的内置库,越深入感觉自己跟不上进度。这篇我们稍微放轻松一些,暂时不讨论管道、调用、进程这些偏系统底层的东西,我们今天研究一下Node中如何调用C/C++语言程序。毕竟单纯用脚本语言想要实现高速的计算还是比较困难的,使用C语言插件可以直接使用很多已有的线性代数库,避免进入重复造轮的境地。

注意:这个话题包含很多概述性质的内容,由于本人很懒,因此很多后续用不到的内容只做提纲挈领的是串烧,不展开叙述,具体细节可以去网上找相关内容。

一、概述

目前Node.js环境下调用主要有以下三种方法:

1. Chrome V8 API

在Node.js起步阶段,实现C++库函数调用,需要直接调用V8内核API。

其代码典型特点如下:

#include <node.h>

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

....

这种方法与V8的版本紧密联系,由于V8引擎底层代码更新太快,很多代码随着V8版本升级直接无法编译,因此除了确实需要进行那些深入骨髓的操作,大概率不推荐这种方法。

2. nan

NAN是 Native Abstractions for Node.js的缩写,是Node.js 为了解决版本更新代码复用而给出的一个接口定义,用于只需要按照给定接口写代码,至于如何转化为版本对应的语句,由NAN来完成。这样用户就告别了直接对V8底层API的直接调用,避免新老版本代码不通用的问题。其代码基本都引用了nan.h这个头文件。

示例程序如下:

#include <nan.h>

void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
  info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}

void Init(v8::Local<v8::Object> exports) {
  v8::Local<v8::Context> context =
      exports->GetCreationContext().ToLocalChecked();
  exports->Set(context,
               Nan::New("hello").ToLocalChecked(),
               Nan::New<v8::FunctionTemplate>(Method)
                   ->GetFunction(context)
                   .ToLocalChecked());
}

NODE_MODULE(hello, Init)

3.node-api

Node-API是Node.js 推出的用于开发 C++ 原生模块的接口,它把所有底层数据结构全部黑盒化,抽象成接口,不同版本的 Node.js 使用同样的接口,只要 ABI 的版本号一致,编译好的 C++ 扩展就可以直接使用,而不需要重新编译。node-api程序有以下特点:

  • 提供头文件 node_api.h;

  • 任何 N-API 调用都返回一个 napi_status 枚举,来表示这次调用成功与否;

  • N-API 的返回值由于被 napi_status 占坑了,所以真实返回值由传入的参数来继承,如传入一个指针让函数操作;

  • 所有 JavaScript 数据类型都被黑盒类型 napi_value 封装,不再是类似于 v8::Objectv8::Number 等类型;

  • 如果函数调用不成功,可以通过 napi_get_last_error_info 函数来获取最后一次出错的信息。

示例程序如下:

#include <assert.h>
#include <node_api.h>

static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

#define DECLARE_NAPI_METHOD(name, func)                                        \
  { name, 0, func, 0, 0, 0, napi_default, 0 }

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

4.node-addon-api

node-addon-api 是构建在ndoe-api 之上,提供了更加简单的 API,使得扩展开发者可以更加容易地编写跨版本、跨平台的扩展。

示例程序如下:

#include <napi.h>

Napi::String Method(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "world");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method));
  return exports;
}

啰嗦这么多,Ctrl+C 手都酸了,总的来说就一句话,目前主流是Node-Api和Node-Addon-Api,后者出现更晚一些。本着学技术新不学旧的观点,我们直接跳过选项 1 2 3 进入node-addon-api的学习。

二、node-gyp

node-gyp是node环境下专门用于编译C/C++文件的程序,是Node环境下构建C++插件的构建工具,和Cmake作用类似,node-gyp读取项目文件下面的名为binding.gyp 的文件,检查构建所需的工具是否齐全、构建环境设置是否到位,自动生成当前环境构建程序所需Makefile等文件。

1.安装

开始之前确保你已经具备了一定node.js基础知识,弄清了node npm npx 这些命令是什么,知道怎么用这些命令基本用法,能够实现前面几篇文章涉及的内容。否则请重新巩固一下基础知识。

此外,由于本人十分厌恶windows下的C开发环境(zhu),后面的实验均是在Linux环境下实现,为此你可能还需要掌握一些linux系统下C语言编程的知识。

使用以下命令

npm -g install node-gyp

其中 -g 代表安装到 node执行文件所在的环境(全局环境)中

2.测试

mkdir js2

cd js2

npm init 

 然后一路回车

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (js2) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /home/fan/work/js2/package.json:

{
  "name": "js2",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": ""
}


Is this OK? (yes) yes

 然后可以看到文件夹下面多了一个 package.json文件

然后安装 node-addon-api

npm install node-addon-api

可以看到文件夹下面多了一个  node_modules 文件夹,node-addon-js就装在这下面

package.json 也多了一项 

# package.json 
"dependencies": {
    "node-addon-api": "^8.3.0"
  }

编写一个最简单的测试文件 hello.cc ,这些文件也可以从Github或  Gitee下载

/** hello.cc **/
#include <napi.h>

Napi::String Method(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "world");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method));
  return exports;
}

NODE_API_MODULE(hello, Init)

和binding.gyp文件

# binding.gyp
{
  "targets": [
    {
      "target_name": "hello_addon", # name of the module
      "sources": [ "hello.cc" ], # source files
      "include_dirs": [ # include directories
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], # disable C++ exceptions
      'cflags!': [ '-fno-exceptions' ],# disable C++ exceptions
      'cflags_cc!': [ '-fno-exceptions' ]   # disable C++ exceptions
    }
  ]
}

项目的含义我已经在备注里标明了,其中<!@ ()  这是 node-gyp的特有的标识符,表明展开括号里语句的执行结果。感兴趣的也可以把括号里的内容复制到shell环境执行以下,看一下结果。

然后执行 配置构建程序

node-gyp configure

大概结果如下:

fan@fan-virtual-machine:~/work/js2$ node-gyp configure
gyp info it worked if it ends with ok
gyp info using node-gyp@11.0.0
gyp info using node@22.11.0 | linux | x64
gyp info find Python using Python version 3.10.12 found at "/usr/bin/python3"

gyp info spawn /usr/bin/python3
gyp info spawn args [
gyp info spawn args '/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args 'binding.gyp',
gyp info spawn args '-f',
gyp info spawn args 'make',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/work/js2/build/config.gypi',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp/addon.gypi',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/.cache/node-gyp/22.11.0/include/node/common.gypi',
gyp info spawn args '-Dlibrary=shared_library',
gyp info spawn args '-Dvisibility=default',
gyp info spawn args '-Dnode_root_dir=/home/fan/.cache/node-gyp/22.11.0',
gyp info spawn args '-Dnode_gyp_dir=/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp',
gyp info spawn args '-Dnode_lib_file=/home/fan/.cache/node-gyp/22.11.0/<(target_arch)/node.lib',
gyp info spawn args '-Dmodule_root_dir=/home/fan/work/js2',
gyp info spawn args '-Dnode_engine=v8',
gyp info spawn args '--depth=.',
gyp info spawn args '--no-parallel',
gyp info spawn args '--generator-output',
gyp info spawn args 'build',
gyp info spawn args '-Goutput_dir=.'
gyp info spawn args ]
gyp info ok 

这么一大坨,我不想看更看不懂,但是我认识 ok ,既然ok 那就可以进行下一步了。

需要注意的是文件夹这时又会多出一个 build 文件,我们构建所需要的文件都在里面。

fan@fan-virtual-machine:~/work/js2/build$ ls -la
total 52
drwxrwxr-x 2 fan fan  4096 12月 17 21:34 .
drwxrwxr-x 4 fan fan  4096 12月 17 21:34 ..
-rw-rw-r-- 1 fan fan   119 12月 17 21:34 binding.Makefile
-rw-rw-r-- 1 fan fan 16296 12月 17 21:34 config.gypi
-rw-rw-r-- 1 fan fan  4248 12月 17 21:34 hello_addon.target.mk
-rw-rw-r-- 1 fan fan 13736 12月 17 21:34 Makefile

 那就构建吧, 在项目文件下 运行

fan@fan-virtual-machine:~/work/js2$ node-gyp build
gyp info it worked if it ends with ok
gyp info using node-gyp@11.0.0
gyp info using node@22.11.0 | linux | x64
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
make: Entering directory '/home/fan/work/js2/build'
  CXX(target) Release/obj.target/hello_addon/hello.o
  SOLINK_MODULE(target) Release/obj.target/hello_addon.node
  COPY Release/hello_addon.node
make: Leaving directory '/home/fan/work/js2/build'
gyp info ok 

 又是一坨,还好最后还是ok 那就说明构建成功了。仔细可看信息提示,最后构建的模块再 build/Release这个文件夹里面,名称就是我们再binding.gyp文件里设置的那个名字。

我们建一个test.js文件来调用一下:

/** test.js **/
let hello_module  = require('./build/Release/hello_addon.node');
// 使用import 会报ERR_UNKNOWN_FILE_EXTENSION 错误
console.log(hello_module.hello());

在shell运行一下:

fan@fan-virtual-machine:~/work/js2$ node test.js 
world

可以看到输出了期望的字符串,说明模块调用成功。

注意这里直接使用 import  方法会报错。然而对于一些代码书写必须用最新的标准强迫症来说,怎么解决?

安装bingdings 模块

npm isntall bindings

 test.js文件中引入该模块

/** test.js **/
//let hello_module  = require('./build/Release/hello_addon.node');
// 使用import 会报ERR_UNKNOWN_FILE_EXTENSION 错误

import bindings from 'bindings'
let  hello_module  =  bindings('hello_addon.node') //bingdings 会自动查找该模块可能存在的位置
console.log(hello_module.hello());

 运行一下:

fan@fan-virtual-machine:~/work/js2$ node --trace-warnings test.js 
world

 结果一样

3.总结

写了那么多我们大概梳理一下 node-addon-api 封包使用的过程

首先是按照规范编写C程序,程序里面要标注导出项有哪些(具体实现后面再说)。

其次是编写binding.gyp文件,给出编译的选项

然后是通过node-gyp编译这些文件

具体使用时 需要通过 require 导入( 或着通过 bindings 模块辅助),使用时与普通函数没有差别。

后面我们将详细探讨 node-addon-api的一些使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值