JS `Node.js` `FFI` (`node-ffi-napi`):加载原生库与调用 C/C++ 函数

各位观众老爷们,大家好!今天咱们不聊八卦,来点硬核的——Node.js FFI (Foreign Function Interface),也就是node-ffi-napi。说白了,就是让咱们的JavaScript代码,也能跟C/C++这些“老家伙”们唠嗑,甚至指挥他们干活。

别害怕,听起来高大上,其实没那么玄乎。咱们一步一步来,保证你听完之后,也能用JS“遥控”C/C++写好的程序。

第一幕:啥是FFI?为啥要用它?

FFI,英文全称Foreign Function Interface,直译过来就是“外部函数接口”。它是一种编程机制,允许一个编程语言调用另一个编程语言编写的函数或代码。 简单来说,就是“跨语言通话”。

那为啥要用它呢?理由可多了:

  • 性能优化: 有些计算密集型任务,C/C++的性能比JS高得多。比如图像处理、音视频编解码等等。把这些任务交给C/C++,JS负责“发号施令”,效率杠杠的。
  • 利用现有资源: 很多成熟的C/C++库已经存在,而且功能强大。与其用JS重写一遍,不如直接用FFI调用,省时省力。
  • 访问底层系统: 有些操作需要直接访问操作系统底层,比如硬件控制、系统调用等。JS本身是做不到的,但C/C++可以。

总而言之,FFI就像一座桥梁,连接了JS的便捷性和C/C++的强大,让我们可以取长补短,实现更强大的功能。

第二幕:node-ffi-napi:我们的“翻译官”

node-ffi-napi是一个Node.js的模块,它就是咱们这座桥梁的“建筑师”,负责搭建JS和C/C++之间的通信通道。 它是node-ffi的继任者,使用N-API, 保证了更好的兼容性和稳定性。

它主要做的事情就是:

  • 加载动态链接库(DLL/SO): 告诉Node.js去哪里找C/C++编译好的库文件。
  • 定义函数签名: 告诉Node.js C/C++函数长啥样,参数类型、返回值类型都是啥。
  • 调用函数: 真正地把JS的请求“翻译”成C/C++能理解的指令,并把C/C++的返回值“翻译”回JS能用的数据。

第三幕:准备工作:磨刀不误砍柴工

在使用node-ffi-napi之前,我们需要做一些准备工作:

  1. 安装node-ffi-napi 这个不用多说,直接npm install ffi-napi

  2. 准备C/C++代码: 这是我们的“演员”,需要先写好C/C++函数,并编译成动态链接库。

    • Windows下是.dll文件。
    • Linux下是.so文件。
    • macOS下是.dylib文件。
  3. 安装ref-napiref-struct-di 这两个模块是node-ffi-napi的依赖,用来处理更复杂的类型,比如结构体。用npm install ref-napi ref-struct-di 安装。

第四幕:实战演练:JS调用C/C++“Hello, World!”

说了这么多理论,不如来点实际的。咱们先从一个简单的例子开始:JS调用C/C++的“Hello, World!”函数。

1. C/C++代码(hello.c):

#include <stdio.h>

#ifdef _WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif

DLLEXPORT void hello() {
    printf("Hello, World! from C/C++n");
}

DLLEXPORT int add(int a, int b) {
    return a + b;
}

DLLEXPORT double multiply(double a, double b) {
    return a * b;
}

2. 编译C/C++代码:

  • Windows: 使用Visual Studio的Developer Command Prompt,输入:

    cl /LD hello.c /Fehello.dll
  • Linux/macOS: 使用GCC,输入:

    gcc -fPIC -shared -o hello.so hello.c

    或者 (macOS)

    gcc -fPIC -shared -o hello.dylib hello.c

3. JS代码(index.js):

const ffi = require('ffi-napi');
const path = require('path');

// 确定动态链接库的路径
const libPath = path.join(__dirname, 'hello'); // Windows下是hello.dll, Linux/macOS下是hello.so/hello.dylib
const os = require('os');
const libExtension = os.platform() === 'win32' ? '.dll' : (os.platform() === 'darwin' ? '.dylib' : '.so');

const fullLibPath = libPath + libExtension;

// 定义C/C++函数的签名
const helloLib = ffi.Library(fullLibPath, {
  'hello': { 'args': [], 'ret': 'void' },
  'add': { 'args': ['int', 'int'], 'ret': 'int' },
  'multiply': { 'args': ['double', 'double'], 'ret': 'double' }
});

// 调用C/C++函数
helloLib.hello(); // 输出:Hello, World! from C/C++

const sum = helloLib.add(10, 20);
console.log(`10 + 20 = ${sum}`); // 输出:10 + 20 = 30

const product = helloLib.multiply(3.14, 2.0);
console.log(`3.14 * 2.0 = ${product}`); // 输出:3.14 * 2.0 = 6.28

4. 运行JS代码:

node index.js

如果一切顺利,你就能在控制台看到C/C++的“Hello, World!”,以及JS调用C/C++函数计算的结果。

第五幕:进阶:处理复杂数据类型

上面的例子很简单,只涉及了基本的数据类型。但实际情况往往更复杂,比如结构体、指针等等。

1. 结构体(Struct):

假设我们的C/C++代码定义了一个结构体:

// struct.c
#include <stdio.h>

#ifdef _WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif

typedef struct {
    int x;
    int y;
} Point;

DLLEXPORT Point createPoint(int x, int y) {
    Point p;
    p.x = x;
    p.y = y;
    return p;
}

DLLEXPORT int getX(Point p) {
  return p.x;
}

我们需要使用ref-struct-di来定义JS中的结构体类型:

// struct.js
const ffi = require('ffi-napi');
const Struct = require('ref-struct-di')(require('ref-napi'));
const path = require('path');

const os = require('os');
const libExtension = os.platform() === 'win32' ? '.dll' : (os.platform() === 'darwin' ? '.dylib' : '.so');
const libPath = path.join(__dirname, 'struct');
const fullLibPath = libPath + libExtension;

// 定义结构体类型
const Point = Struct({
    'x': 'int',
    'y': 'int'
});

const PointType = Point; // 显式定义类型,方便使用

// 定义C/C++函数的签名
const structLib = ffi.Library(fullLibPath, {
    'createPoint': { 'args': ['int', 'int'], 'ret': PointType },
    'getX': {'args': [PointType], 'ret': 'int'}
});

// 调用C/C++函数
const point = structLib.createPoint(100, 200);
console.log(point); // 输出:{ x: 100, y: 200 }
console.log(structLib.getX(point)); // 输出: 100

2. 指针(Pointer):

指针是C/C++的灵魂,也是FFI的难点之一。我们需要使用ref-napi来处理指针。

假设我们的C/C++代码需要传递一个字符串指针:

// pointer.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#ifdef _WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif

DLLEXPORT char* duplicateString(const char* str) {
    if (str == NULL) {
        return NULL;
    }

    char* newStr = (char*)malloc(strlen(str) + 1);
    if (newStr == NULL) {
        return NULL;
    }

    strcpy(newStr, str);
    return newStr;
}

DLLEXPORT void freeString(char* str) {
  free(str);
}

在JS中,我们需要使用ref-napi来分配内存,并将字符串写入内存:

// pointer.js
const ffi = require('ffi-napi');
const ref = require('ref-napi');
const path = require('path');

const os = require('os');
const libExtension = os.platform() === 'win32' ? '.dll' : (os.platform() === 'darwin' ? '.dylib' : '.so');
const libPath = path.join(__dirname, 'pointer');
const fullLibPath = libPath + libExtension;

// 定义C/C++函数的签名
const pointerLib = ffi.Library(fullLibPath, {
    'duplicateString': { 'args': ['string'], 'ret': 'string' },
    'freeString': {'args': ['string'], 'ret': 'void'}
});

// 调用C/C++函数
const originalString = "Hello, FFI!";
const duplicatedString = pointerLib.duplicateString(originalString);
console.log(`Original string: ${originalString}`);
console.log(`Duplicated string: ${duplicatedString}`);

// 记得释放C/C++分配的内存
pointerLib.freeString(duplicatedString); //重要: 释放内存,否则会内存泄漏

第六幕:错误处理:防患于未然

在使用FFI时,错误处理非常重要。C/C++代码可能会出错,我们需要在JS中捕获这些错误,并进行处理。

  • C/C++代码中的错误: 可以在C/C++代码中使用错误码,并通过返回值传递给JS。
  • node-ffi-napi的错误: node-ffi-napi可能会抛出异常,我们需要使用try...catch语句来捕获这些异常。

第七幕:注意事项:小心驶得万年船

  • 内存管理: C/C++代码分配的内存,需要手动释放。否则会造成内存泄漏。
  • 类型匹配: JS和C/C++的数据类型必须匹配。否则会导致程序崩溃。
  • 线程安全: 如果你的C/C++代码是多线程的,需要考虑线程安全问题。
  • 平台差异: 不同平台的动态链接库的格式不同,需要根据平台选择正确的库文件。
  • N-API版本: 确保node-ffi-napi使用的N-API版本与Node.js版本兼容。

第八幕:总结:举一反三,融会贯通

今天咱们聊了Node.js FFI,特别是node-ffi-napi的使用。从简单的“Hello, World!”到复杂的数据类型处理,再到错误处理和注意事项,希望你能对FFI有一个更清晰的认识。

FFI是一个强大的工具,可以让你在JS中利用C/C++的强大能力。但同时,它也需要谨慎使用,注意内存管理、类型匹配等问题。

记住,编程没有捷径,只有不断学习和实践。希望今天的讲座能对你有所帮助,祝你编程愉快!

发表回复

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