electron教程(三): 使用ffi-napi引入C++的dll


我的electron教程系列

electron教程(一): electron的安裝和項目的創建

electron教程(番外篇一): 開發環境及插件, VSCode調試, ESLint + Google JavaScript Style Guide代碼規范

electron教程(番外篇二): 使用TypeScript版本的electron, VSCode調試TypeScript, TS版本的ESLint

electron教程(二): http服務器, ws服務器, 子進程管理

electron教程(三): 使用ffi-napi引入C++的dll

electron教程(四): 使用electron-builder或electron-packager將項目打包為可執行桌面程序(.exe)

 

引言

 

這一篇將介紹如何在node.js+electron環境中, 使用node-ffi/ffi-napi調用C/C++編寫的動態鏈接庫(即dll), 實現調用C/C++代碼.

本教程適用於electron 4.x-6.x版本.

如electron 4.2.10版本, electron 5.0.6版本, electron 6.0.10版本.

 

ffi

 

實現這個功能, 主要使用的插件是ffi.

node-ffi是一個用於使用純JavaScript加載和調用動態庫的Node.js插件。它可以用來在不編寫任何C++代碼的情況下創建與本地DLL庫的綁定。同時它負責處理跨JavaScript和C的類型轉換。

node-ffi連接了C代碼和JS代碼, 通過內存共享來完成調用, 而內部又通過ref,ref-arrayref-struct來實現類型轉換.

 

安裝 ffi-napi

 

ffi-napi是作者(node-ffi-napi)根據node-ffi修改而發布到npm倉庫的, 可以直接通過npm安裝, 支持node.js 12和electron高版本.

ffi-napi詳情見: ffi-napi的github頁面

node-ffi是ffi的官方版本, 但是不能用在我們的項目中, 如果你對它失敗的原因感興趣, 我寫在了本文的最后一節.

 

1. 部署node.js+electron環境

按步驟完成electron教程(一): electron的安裝和項目的創建所介紹的內容.

 

2. 安裝ffi-napi

執行指令:

yarn add ffi-napi

 

使用ffi-napi

 

main.js中添加如下代碼:

const ffi = require('ffi-napi');
/**
 * 先定義一個函數, 用來在窗口中顯示字符
 * @param {String} text
 * @return {*} none
 */
function showText(text) {
  return new Buffer(text, 'ucs2').toString('binary');
};
// 通過ffi加載user32.dll
const myUser32 = new ffi.Library('user32', {
  'MessageBoxW': // 聲明這個dll中的一個函數
    [
      'int32', ['int32', 'string', 'string', 'int32'], // 用json的格式羅列其返回類型和參數類型
    ],
});

// 調用user32.dll中的MessageBoxW()函數, 彈出一個對話框
const isOk = myUser32.MessageBoxW(
    0, showText('I am Node.JS!'), showText('Hello, World!'), 1
);
console.log(isOk);

這段代碼中, 主要調用了windows的user32.dll, 具體的步驟都寫在了代碼的注釋中.

啟動程序:

npm start

彈窗出現, Hello World!

 

自己生成一個dll

 

0. 首先要明確一點的就是:

ffi只接受純C函數, 確切的說, 是按照C標准編譯的函數

下面來說說具體的原因:

在通過ffi引入dll的時候, 我們是這么聲明的:

const myUser32 = new ffi.Library('user32', {
  'MessageBoxW': // 聲明這個dll中的一個函數
    [
      'int32', ['int32', 'string', 'string', 'int32'], // 用json的格式羅列其返回類型和參數類型
    ],
});

user32.dll中, 尋找一個名字叫MessageBoxW的函數.

但是, C和C++的函數命名是不同的, 我指的是編譯后的函數名字

對於C, 函數int func(int n)會被編譯為類似_func這樣的名字.

對於C++, 函數int func(int n)會被編譯為類似?func@@YAHH@Z這樣的名字.

同樣是C++, 函數int func(int double)會被編譯為類似?func@@YAHN@Z這樣的名字(和上一個不同).

名字中包含了較多信息, 比如:

參數的入棧方式
返回值的類型
參數的類型和數量

這是因為C++有函數重載特性, 雖然函數命名是func, 但int func(int n)int func(int double)完全是兩個不同的函數, 編譯器通過給它們賦予不同的名字來區分它們.

如果你感興趣, 這里有更詳細的解釋

回到ffi, 它在dll中查找函數名字的時候, 是用C風格來查找的.

所以如果你的函數使用C++編譯的, ffl在這個dll中就找不到這個函數, 錯誤LINK 126!

1. 創建工程

使用VS創建一個C++空項目即可. 項目名成以myAddDll為例.

當然, 你也可以直接創建動態鏈接庫DLL.

 

2.修改配置類型為動態庫.dll


如圖:

在項目配置中, 選擇生成動態庫.dll

確保你配置了Debug和Release, 同時確保你在x64環境下生成.

 

3. 函數聲明

創建一個myAdd.h頭文件

聲明一個函數:

extern "C"
{
    __declspec(dllexport) int funAdd(int a, int b);
}

extern "C"意味着:

被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和鏈接的

__declspec(dllexport) 意味着:

__declspec(dllexport)用於Windows中的動態庫中,聲明導出函數、類、對象等供外面調用,省略給出.def文件。即將函數、類等聲明為導出函數,供其它程序調用,作為動態庫的對外接口函數、類等。

 

4. 函數定義

創建一個myAdd.cpp文件

定義funAdd函數:

#include "myAdd.h"
int funAdd(int a, int b)
{
  return (a + b);
}

函數的內容很簡單, 接受兩個int類型參數, 返回它們的和.

 

5. 生成dll

右鍵項目選擇生成即可, 生成的myAddDll.dll位於項目目錄下的x64/Debug中.

(根據你項目的配置去找, x64或x86, Debug或Release)

 

6. 測試dll

myAddDll.dll拷貝至你的electron項目的根目錄下的dll文件夾內

在main.js中添加如下代碼:

const ffi = require('ffi-napi'); // 如果前面已經定義過ffi, 就注釋掉這一行
// 通過ffi加載myAddDll.dll
const myAddDll = new ffi.Library('../dll/myAddDll', {
  'funAdd': // 聲明這個dll中的一個函數
    [
      'int', ['int', 'int'], // 用json的格式羅列其返回類型和參數類型
    ],
});

// 調用函數, 參數1和2, 將返回值直接打印出來, 預計為3
const result = myAddDll.funAdd(1, 2);
console.log(`the result of 1 + 2 is: `+ result);

這段代碼中, 主要調用了myAddDll.dll中的int funAdd(int, int), 具體的步驟都寫在了代碼的注釋中.

啟動程序:

npm start

查看cmd中的日志:

the result of 1 + 2 is: 3

 

6.1 可能的錯誤

LINK 126

這個錯誤, 意味者electron無法使用你的dll.

在這行代碼中

const myAddDll = new ffi.Library('../dll/myAddDll', {

ffi.Library的第一個參數, 不光指定了dll的名字, 還指定了dll的路徑.

出現LINK 126有兩個常見原因:

1. 沒有這個目錄, 或這個目錄下沒有myAddDll.dll

2. myAddDll.dll還依賴了其他的一些dll, 但是electron無法找到這個dll.

LINK 127

出現LINK 127的可能原因:

1. electron找到了你的dll, 但是在dll中找不到你聲名的函數(funAdd).這通常是由於函數名字錯誤, 或者是返回值類型/參數的個數及類型不一致導致的.

node-ffi為什么會安裝失敗

 

如果我們按照node-ffi的github鏈接中介紹的方法來安裝ffi, 即

npm install ffi

然后嘗試在main.js中加上一句代碼來導入這個模塊,

const ffi = require('ffi');

運行一下

npm start

你會得到, 一個錯誤!

仔細看看提示:

The module xxxxxx was compiled against a different Node.js version using
NODE_MODULE_VERSION 69. This version of Node.js requires
NODE_MODULE_VERSION 73

簡單地說:

這個模塊是用一個NODE_MODULE_VERSION 69的node.js版本進行編譯的,
而當前的版本的Node.js需要NODE_MODULE_VERSION 73(來進行編譯).

那么NODE_MODULE_VERSION是什么意思? 后面的數字又是什么意思?

我們在這里可以查詢到, 各個NODE_MUODULE_VERSION對應的node.js版本或electron版本, 也叫做node_abi.

再翻譯一下錯誤提示:

這個模塊是用一個electron 4.0.4 版本進行編譯的,
而當前的版本需要electron 6(來進行編譯).

目前為止, 我們的問題解決方案:

重新編譯.

重新編譯, 指定electron版本, 執行指令:

npm rebuild --runtime=electron --target=6.0.10 --disturl=https://atom.io/download/atom-shell --abi=73

注: 從https://atom.io/download/atom-shell下載會比較慢, 請自備梯子, 或者使用淘寶庫來下載.

日志中打印出了多條error, 原因是:

node-ffi里面會調用v8或其他依賴模塊的接口, 而這些接口已經更新了, 有的接口改了名字, 有的接口改了參數數量. 但是node-ffi的調用接口語句並沒有更新, 所以編譯不過.

進一步的問題解決方案:

修改node-ffi的代碼, 以適應新版本的v8, 和其他依賴模塊.

一般而言, 我建議翻閱github中該項目的的issues, 在關於編譯和electron的話題中, 你會找到其他人已經修改好的代碼, 通常是一個該項目的fork版本. 你需要下載這個fork版本的源碼, 將它拷貝至你的項目node_modules文件夾中, 使用上面的編譯指令編譯安裝.

而前文介紹的ffi-napi是一個特殊情況, 作者不光修改了node-ffi. 還把它修改后的代碼上傳到了npm倉庫, 所以我們可以通過npm install ffi-napi來進行安裝, 不必按照下載-拷貝-編譯的流程來安裝它.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM