руководство по использованию node-ffi

Node.js JavaScript C++

существуетnodejs/elctron, пройти можноnode-ffi,пройти черезForeign Function InterfaceВызов библиотеки динамической компоновки, обычно известный как вызов DLL, реализует вызов кода C/C++, чтобы реализовать многие функции, которые node не легко реализовать, или повторно использовать многие функции, которые были реализованы.

node-ffi — это плагин Node.js для загрузки и вызова динамических библиотек с использованием чистого JavaScript. Его можно использовать для создания привязок к собственным библиотекам DLL без написания кода на C++. Он также обрабатывает преобразования типов в JavaScript и C.

а такжеNode.js AddonsПо сравнению с этим методом, этот метод имеет следующие преимущества:

1. 不需要源代码。
2. 不需要每次重编译`node`,`Node.js Addons`引用的`.node`会有文件锁,会对`electron应用热更新造成麻烦。
3. 不要求开发者编写C代码,但是仍要求开发者具有一定C的知识。

слабость это:

1. 性能有折损
2. 类似其他语言的FFI调试,此方法近似黑盒调用,差错比较困难。

Установить

node-ffiпройти черезBufferкласс, который реализует совместное использование памяти между кодом C и кодом JS, а преобразование типов выполняется черезref,ref-array,ref-structвыполнить. из-заnode-ffi/refСодержит собственный код C, поэтому при установке необходимо настроить среду компиляции собственных подключаемых модулей Node.

// 管理员运行bash/cmd/powershell,否则会提示权限不足
npm install --global --production windows-build-tools
npm install -g node-gyp

Установите соответствующие библиотеки по мере необходимости

npm install ffi
npm install ref
npm install ref-array
npm install ref-struct

еслиelectronпроект, проект может установить плагин для восстановления электронов, который можно легко пройтиnode-modulesвсе необходимоеrebuildБиблиотека перекомпилирована.

npm install electron-rebuild

Настройте ярлыки в package.json

package.json
    "scripts": {
    "rebuild": "cd ./node_modules/.bin && electron-rebuild --force --module-dir=../../"
}

выполнить послеnpm run rebuildоперация завершенаelectronперекомпилировать.

Простой пример

extern "C" int __declspec(dllexport)My_Test(char *a, int b, int c);
extern "C" void __declspec(dllexport)My_Hello(char *a, int b, int c);
import ffi from 'ffi'
// `ffi.Library`用于注册函数,第一个入参为DLL路径,最好为文件绝对路径
const dll = ffi.Library( './test.dll', {
    // My_Test是dll中定义的函数,两者名称需要一致
    // [a, [b,c....]] a是函数出参类型,[b,c]是dll函数的入参类型
    My_Test: ['int', ['string', 'int', 'int']], // 可以用文本表示类型
    My_Hello: [ref.types.void, ['string', ref.types.int, ref.types.int]] // 更推荐用`ref.types.xx`表示类型,方便类型检查,`char*`的特殊缩写下文会说明
})

//同步调用
const result = dll.My_Test('hello', 3, 2)

//异步调用
dll.My_Test.async('hello', 3, 2, (err, result) => {
    if(err) {
        //todo
    }
    return result
})

тип переменной

В языке C есть 4 основных типа данных: целочисленный, с плавающей запятой, указатель и агрегатный тип.

Основание

Существует два типа целочисленных и символьных типов: со знаком и без знака.

Типы Минимальный диапазон
char 0 ~ 127
signed char -127 ~ 127
unsigned char 0 ~ 256

По умолчанию используется подписанный тип, если unsigned не объявлен

refсерединаunsignedбудет сокращено доu, Такие какucharвести перепискуusigned char.

с плавающей запятойfloat double long double.

refБиблиотека подготовила для нас соответствующие отношения базовых типов.

типы С++ ref соответствующий тип
void ref.types.void
int8 ref.types.int8
uint8 ref.types.uint8
int16 ref.types.int16
uint16 ref.types.uint16
float ref.types.float
double ref.types.double
bool ref.types.bool
char ref.types.char
uchar ref.types.uchar
short ref.types.short
ushort ref.types.ushort
int ref.types.int
uint ref.types.uint
long ref.types.long
ulong ref.types.ulong
DWORD ref.types.ulong

DWORD этоwinapiтип, подробно ниже

Дополнительные расширения могут пойти вref doc

ffi.Library, вы можете объявить тип либо с помощью ref.types.xxx, либо с помощью текста (например,uint16) объявить.

тип персонажа

тип персонажа поcharсоставляют, вGBKКитайский иероглиф занимает 2 байта в кодировке и 3~4 байта в UTF-8. Одинref.types.charПо умолчанию один байт. Создайте пространство памяти, достаточно длинное в соответствии с требуемой длиной символа. В это время вам нужно использоватьref-arrayбиблиотека.

const ref = require('ref')
const refArray = require('ref-array')

const CharArray100 = refArray(ref.types.char, 100) // 申明char[100]类型CharArray100
const bufferValue = Buffer.from('Hello World') // Hello World转换Buffer
// 通过Buffer循环复制, 比较啰嗦
const value1 = new CharArray100()
for (let i = 0, l = bufferValue.length; i < l; i++) {
    value1[i] = bufferValue[i]
}
// 使用ref.alloc初始化类型
const strArray = [...bufferValue] //需要将`Buffer`转换成`Array`
const value2 = ref.alloc(CharArray100, strArray)

При передаче китайских иероглифов необходимо знать заранееDLLКак кодируется библиотека. Node по умолчанию использует кодировку UTF-8. Если DLL не в кодировке UTF-8, ее необходимо перекодировать, рекомендуется использоватьiconv-lite

npm install iconv-lite
const iconv = require('iconv-lite')
const cstr = iconv.encode(str, 'gbk')

Уведомление! После перекодирования с помощью encodecstrдляBufferкласс, который может быть непосредственно использован какucharТипы

В iconv.encode(str.'gbk') gbk использует по умолчаниюunsigned char | 0 ~ 256хранить. Если код C должен бытьsigned char | -127 ~ 127, вам нужно преобразовать данные в буфере в тип int8.

const Cstring100 = refArray(ref.types.char, 100)
const cString = new Cstring100()
const uCstr = iconv.encode('农企药丸', 'gbk')
for (let i = 0; i < uCstr.length; i++) {
    cString[i] = uCstr.readInt8(i)
}

Код C для массива символовchar[]/char *Возвращаемое значение параметра, обычно возвращаемый текст, не имеет фиксированной длины, предварительно выделенное пространство не будет использоваться полностью, а конец будет бесполезным значением. Если это предварительно инициализированное значение, в конце обычно имеется большая строка.0x00, что нужно сделать вручнуюtrimEnd, если это не предварительно инициализированное значение, значение в конце не определено, и код C должен явно возвращать длину массива строкreturnValueLength.

встроенная стенография

В ffi встроены некоторые сокращения.

ref.types.int => 'int'
ref.refType('int') => 'int*'
char* => 'string'

Рекомендуется только «строка».

Хотя в js строки считаются базовыми типами, в языке C они представлены в виде объектов, поэтому считаются ссылочными типами. такstringФактическиchar* вместоchar

Тип агрегации

Многомерные массивы

Если вы столкнулись с базовым типом, определенным как многомерный массив, вам нужно использовать ref-array для его создания.

    char cName[50][100] // 创建一个cName变量储存级50个最大长度为100的名字
    const ref = require('ref')
    const refArray = require('ref-array')

    const CName = refArray(refArray(ref.types.char, 100), 50)
    const cName = new CName()

структура

Структура является широко используемым типом в C и должна использоватьсяref-structсоздавать

typedef struct {
    char cTMycher[100];
    int iAge[50];
    char cName[50][100];
    int iNo;
} Class;

typedef struct {
    Class class[4];
} Grade;
const ref = require('ref')
const Struct = require('ref-struct')
const refArray = require('ref-array')

const Class = Struct({  // 注意返回的`Class`是一个类型
    cTMycher: RefArray(ref.types.char, 100),
    iAge: RefArray(ref.types.int, 50),
    cName: RefArray(RefArray(ref.types.char, 100), 50)
})
const Grade = Struct({ // 注意返回的`Grade`是一个类型
    class: RefArray(Class, 4)
})
const grade3 = new Grade() // 新建实例

указатель

Указатель — это переменная, значением которой является адрес фактической переменной, т. е. прямой адрес ячейки памяти, чем-то похожий на ссылочный объект в JS.

используется в языке C*представлять указатель

Напримерint a* — указатель на переменную целочисленного типа a ,&используется для обозначения адреса

int a=10,
int *p; // 定义一个指向整数型的指针`p`
p=&a // 将变量`a`的地址赋予`p`,即`p`指向`a`

node-ffiПринцип реализации указателей заключается в использованииref,использоватьBufferКлассы реализуют совместное использование памяти между кодом C и кодом JS, что позволяетBufferСтаньте указателем на языке C. Обратите внимание, что после цитированияref, изменитBufferизprototype, замените и внедрите некоторые методы, обратитесь к документациисправочная документация

const buf = new Buffer(4) // 初始化一个无类型的指针
buf.writeInt32LE(12345, 0) // 写入值12345

console.log(buf.hexAddress()) // 获取地址hexAddress

buf.type = ref.types.int // 设置buf对应的C类型,可以通过修改`type`来实现C的强制类型转换
console.log(buf.deref()) // deref()获取值12345

const pointer = buf.ref() // 获取指针的指针,类型为`int **`

console.log(pointer.deref().deref())  // deref()两次获取值12345

Чтобы было понятно о двух концепциях, одна — это тип структуры, а другая — тип указателя, который объясняется в коде.

// 申明一个类的实例
const grade3 = new Grade() // Grade 是结构类型
// 结构类型对应的指针类型
const GradePointer = ref.refType(Grade) // 结构类型`Grade`对应的指针的类型,即指向Grade
// 获取指向grade3的指针实例
const grade3Pointer = grade3.ref()
// deref()获取指针实例对应的值
console.log(grade3 === grade3Pointer.deref())  // 在JS层并不是同一个对象
console.log(grade3['ref.buffer'].hexAddress() === grade3Pointer.deref()['ref.buffer'].hexAddress()) //但是实际上指向的是同一个内存地址,即所引用值是相同的

в состоянии пройтиref.alloc(Object|String type, ? value) → Bufferполучить ссылочный объект напрямую

const iAgePointer = ref.alloc(ref.types.int, 18) // 初始化一个指向`int`类的指针,值为18
const grade3Pointer = ref.alloc(Grade) // 初始化一个指向`Grade`类的指针

Перезвоните

Функции обратного вызова C обычно используются в качестве входных параметров.

const ref = require('ref')
const ffi = require('ffi')

const testDLL = ffi.Library('./testDLL', {
    setCallback: ['int', [
        ffi.Function(ref.types.void,  // ffi.Function申明类型, 用`'pointer'`申明类型也可以
        [ref.types.int, ref.types.CString])]]
})


const uiInfocallback = ffi.Callback(ref.types.void, // ffi.callback返回函数实例
    [ref.types.int, ref.types.CString],
    (resultCount, resultText) => {
        console.log(resultCount)
        console.log(resultText)
    },
)

const result = testDLL.uiInfocallback(uiInfocallback)

Уведомление! Если ваш CallBack вызывается в setTimeout, может быть ошибка GC

process.on('exit', () => {
    /* eslint-disable-next-line */
    uiInfocallback // keep reference avoid gc
})

пример кода

Приведите пример полной цитаты

// 头文件
#pragma  once

//#include "../include/MacroDef.h"
#define	CertMaxNumber 10
typedef struct {
	int length[CertMaxNumber];
	char CertGroundId[CertMaxNumber][2];
	char CertDate[CertMaxNumber][2048];
}  CertGroud;

#define DLL_SAMPLE_API  __declspec(dllexport)

extern "C"{

//读取证书
DLL_SAMPLE_API  int My_ReadCert(char *pwd, CertGroud *data,int *iCertNumber);
}
const CertGroud = Struct({
    certLen: RefArray(ref.types.int, 10),
    certId: RefArray(RefArray(ref.types.char, 2), 10),
    certData: RefArray(RefArray(ref.types.char, 2048), 10),
    curCrtID: RefArray(RefArray(ref.types.char, 12), 10),
})

const dll = ffi.Library(path.join(staticPath, '/key.dll'), {
    My_ReadCert: ['int', ['string', ref.refType(CertGroud), ref.refType(ref.types.int)]],
})

async function readCert({ ukeyPassword, certNum }) {
    return new Promise(async (resolve) => {
        // ukeyPassword为string类型, c中指代 char*
        ukeyPassword = ukeyPassword.toString()
        // 根据结构体类型 开辟一个新的内存空间
        const certInfo = new CertGroud()
        // 开辟一个int 4字节内存空间
        const _certNum = ref.alloc(ref.types.int)
        // certInfo.ref()作为certInfo的指针传入
        dll.My_ucRMydCert.async(ukeyPassword, certInfo.ref(), _certNum, () => {
            // 清除无效空字段
            let cert = bufferTrim.trimEnd(new Buffer(certInfo.certData[certNum]))
            cert = cert.toString('binary')
            resolve(cert)
        })
    })
}

Распространенные ошибки

  • Dynamic Linking Error: Win32 error 126

Есть три причины этой ошибки

  1. Обычно входящий путь DLL неверен, и файл DLL не может быть найден, рекомендуется использовать абсолютный путь.
  2. если на х64node/electronСледующая ссылка на 32-разрядную DLL также сообщит об этой ошибке, и наоборот. Убедитесь, что DLL требует той же архитектуры ЦП, что и среда выполнения.
  3. Библиотека DLL также ссылается на другие файлы DLL, но указанный файл DLL не может быть найден. Это может быть библиотека зависимостей VC или отношение зависимости между несколькими библиотеками DLL.
  • Ошибка динамической компоновки: ошибка Win32 127: Функция с соответствующим именем не найдена в DLL.Необходимо проверить, совпадает ли имя функции, определенное в заголовочном файле, с именем функции, записанным при вызове DLL.

Настройка пути

Если у вас несколько DLL и есть проблема с обращением друг к другу, появитсяDynamic Linking Error: Win32 error 126Ошибка 3. Это связано с процессом по умолчаниюPathэто каталог, в котором находится двоичный файл, т.е.node.exe/electron.exeКаталог не является каталогом, в котором находится DLL, поэтому невозможно найти другие ссылки в том же каталоге, что и DLL. Ее можно решить следующими методами:

//方法一, 调用winapi SetDllDirectoryA设置目录
const ffi = require('ffi')

const kernel32 = ffi.Library("kernel32", {
'SetDllDirectoryA': ["bool", ["string"]]
})
kernel32.SetDllDirectoryA("pathToAdd")

//方法二(推荐),设置Path环境环境
process.env.PATH = `${process.env.PATH}${path.delimiter}${pathToAdd}`

Инструмент анализа DLL

Инструменты, которые могут просматривать всю информацию о библиотеках ссылок DLL и зависимостях DLL, но, к сожалению, не поддерживаютWIN10. если вы неWIN10Пользователи, тогда вам нужен только этот один инструмент, а следующие инструменты можно пропустить.

Вы можете просматривать различные операции во время выполнения процесса, такие как ввод-вывод, доступ к реестру и т. д. используйте его здесь для мониторингаnode/electronОбработка операций ввода-вывода для устранения неполадокDynamic Linking Error: Win32 errorПричина ошибки 3, вы можете просмотретьffi.LibaryВсе запросы ввода-вывода и соответствующие результаты в то время, чтобы увидеть, чего не хватаетDLL.

dumpbin.exe — это двоичный преобразователь Microsoft COFF, который отображает информацию о двоичных файлах Common Object File Format (COFF). Объектные файлы COFF, стандартные объектные библиотеки COFF, исполняемые файлы и библиотеки динамической компоновки можно проверить с помощью dumpbin. Запуск через меню «Пуск» -> Visual Studio 20XX -> Инструменты Visual Studio -> Собственная командная строка VS20XX x86.

dumpbin /headers [dll路径] // 返回DLL头部信息,会说明是32 bit word Machine/64 bit word Machine
dumpbin /exports [dll路径] // 返回DLL导出信息,name列表为导出的函数名

проблема со сбоем флешки

действительныйnode-ffiПри отладке легко могут возникнуть сбои флэш-памяти из-за ошибок памяти, и даже точки останова могут вызвать сбои. Это часто вызвано несанкционированным доступом к памяти, к которому можно получить доступ черезWindowsЖурнал видит сообщение об ошибке, но поверьте мне, это не помогает. Ошибки памяти в C — непростая проблема.

приложение

инструмент автоматического преобразования

tjfontaine предоставилnode-ffi-generate, который может быть сгенерирован автоматически на основе заголовочного файлаnode-ffiОбъявление функции, обратите внимание на эту необходимостьLinuxОкружающая среда, просто используйте KOA, чтобы упаковать слой и перевести его в онлайн-режим.ffi-online

WINAPI

node-win32-apiwindef.h

GetLastError

node-ffiGetLastErrorC++ addon

GetLastError() always 0 when using Win32 API

FFFFFFFF

pvoidnode-ffiFFFFFFFFderef()

HDEVNOTIFY
WINAPI
RegisterDeviceNotificationA(
    _In_ HANDLE hRecipient,
    _In_ LPVOID NotificationFilter,
    _In_ DWORD Flags);

HDEVNOTIFY hDevNotify = RegisterDeviceNotificationA(hwnd, &notifyFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
if (!hDevNotify) {
	DWORD le = GetLastError();
	printf("RegisterDeviceNotificationA() failed [Error: %x]\r\n", le);
	return 1;
}
const apiDef = SetupDiGetClassDevsW: [W.PVOID_REF, [W.PVOID, W.PCTSTR, W.HWND, W.DWORD]] // 注意返回类型`W.PVOID_REF`必须设置成pointer,就是不设置type,则node-ffi不会尝试`deref()`
const hDEVINFOPTR = this.setupapi.SetupDiGetClassDevsW(null, typeBuffer, null,
    setupapiConst.DIGCF_PRESENT | setupapiConst.DIGCF_ALLCLASSES
)
const hDEVINFO = winapi.utils.getPtrValue(hDEVINFOPTR, W.PVOID) // getPtrValue特判,如果地址为全`FF`则返回空
if (!hDEVINFO) {
    throw new ErrorWithCode(ErrorType.DEVICE_LIST_ERROR, ErrorCode.GET_HDEVINFO_FAIL)
}