Electron で Windows アプリを作ろうとして node-ffi でハマった話


やりたかったこと

Electron で Windows アプリを作り、 Electron 側から既成の dll が使えることを確認することです。

ゴールとしては、

  1. C++ で "Hello, world!" という文字列を返却する HelloWorld 関数を作る
  2. Electron(JavaScript)側から HelloWorld 関数を呼び出し、alert で表示する

というものです。

ハマったポイント

npm で node-ffi をインストール後、JavaScript 側で require('ffi') をやるとエラーが発生します。これがなかなか解消できず、ハマりました。

↓こういうエラー

Uncaught Error: A dynamic link
library (DLL) initialization routine failed.

厄介なのは、JavaScript として、【require('ffi')】を呼び出した段階でエラーになってしまうこと。なぜなら、ffi を npm でインストールしただけ だから。
こうなると、インストールしただけではダメなのか? という疑問が出てくる。

いろいろ調べると・・・

npm でインストールしただけでは使えなさそう な臭いがプンプンしてきます。
で、だいたい出てくる情報は、

Electron で使っている V8 エンジンが Node.js の V8 エンジンと違うので、Electron 用にパッケージをリビルドしないといけない

という情報です。
で、それについて調べると出てくるのが、

electron-rebuild

 
です。

StackOverflow でも「解決策見つけたよ」と言っていた人が ここ を紹介していて、
ページの中段辺りの Installing modules and rebuilding for Electron で思いっきり書いてある、と。

そうなると期待を膨らませて実行するわけですよ。

$ npm install electron-rebuild
$ ./node_modules/.bin/electron-rebuild
$ レRebuild Complete

よし!リビルド完了!(★注目★)

$ ./node_modules/.bin/electron .

Electron 実行!

Uncaught Error: A dynamic link
library (DLL) initialization routine failed.

ぐぬぬ!

割と最近の記事で、↓の記事の人も electron-rebuild やっている・・・。うーん・・・。
【Electron】Felica連携デスクトップアプリケーションの開発

というわけで、真にハマったところは、この electron-rebuild なのです。

そして解決へ

こういうダダハマリした場合って、けっこう手前(前提的なところ)に落とし穴があって、
今回もその例から外れることなく、結果から見ると非常に単純な理由でした。

【(★注目★)】と書いたところの electron-rebuild

こいつ、リビルドしてないぞ!

リビルドって言ってる割には一瞬で終わるのですよ、electron-rebuild コマンド。

で、いろいろ試した結果、どうすれば electron-rebuild でリビルドしてくれるのかというと、

npm でインストールするときに -D(--save-dev) を付けない

ということです。
そして残念なことに

-g でのグローバルインストールで統一してもダメ

です。(一番解決できそうな組み合わせなのに・・・)

で、結論です。

★ node-ffi だけは npm install で -g, -D(--save-dev) を付けない ★

です。

実際にやったことなど

まず、状況の確認

  • Windows 10 Pro 1803(April 2018 Update)
  • Visual Studio 2017 Community(VC++インストール済み)
  • Node.js 8.11.4(64ビット版)

↓Node バージョンやパッケージの状態↓

$ node -v
v8.11.4
$ npm -v
6.4.1
$ npm list -g --depth=0
npm
$ npm list --depth=0
なし

パッケージをインストールする

$ npm i electron
$ npm i electron-rebuild
$ npm i ffi

↓インストールした結果の package.json↓

package.json
{
  "name": "electron_ffi_sample",
  "main": "main.js",
  "author": "odorry",
  "scripts": {
    "start": "node_modules/.bin/electron ."
  },
  "dependencies": {
    "electron": "^2.0.9",
    "electron-rebuild": "^1.8.2",
    "ffi": "^2.2.0"
  }
}

npm i(install) でインストールしたので、
dependencies のところにインストールしたものが書かれている。

Electron の V8 エンジン用にリビルドする

これですね。肝は。
このコマンドで、リビルドにある程度の時間がかかれば OK です。
(5 〜 10 秒くらいで、少なくとも一瞬ではない。)

$ ./node_modules/.bin/electron-rebuild
レ Rebuild Complete

Visual Studio の VC++ で dllSample.dll を作成する

Visual Studio の使い方になってしまうので、詳細は割愛しますが、
Visual Studio でビルドして DllSample.dll を手に入れます。

DllSample.h
#include <iostream>

extern "C" __declspec(dllexport) const char * HelloWorld();
DllSample.cpp
#include "stdafx.h"
#include "DllSample.h"

const char * HelloWorld()
{
    const char *cstring = "Hello, World!";
    return cstring;
}

これをビルドして DllSample.dll を手に入れてください。

Electron アプリ(html)側を用意する

ファイル構造はこのような感じです

任意のフォルダ
┣ node_modules      // npm install でインストールされたパッケージ
┣ DllSample.dll     // Visual Studio 側で作成した C++ の DLL
┣ index.html        // Electron アプリの最初の画面
┣ main.js           // Electron アプリの起動スクリプト
┣ package.json      // Node パッケージ用のファイル
┗ package-lock.json // npm install で勝手に作られるファイル
main.js
const {app, BrowserWindow} = require('electron')

let win

app.on('ready', () => {
  win = new BrowserWindow({width: 800, height: 600})

  win.loadURL('file://' + __dirname + '/index.html')

  win.on('closed', () => {
    win = null
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (win === null) {
    createWindow()
  }
})
index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>DLL呼び出しサンプル</title>
</head>
<body>
    <script>
        var ffi = require('ffi');

        var dllSamplePath = "./DllSample.dll";
        var dllSample = ffi.Library(dllSamplePath, {
            'HelloWorld': ['string', []]
        });

        var result = dllSample.HelloWorld();
        alert(result);
    </script>
</body>
</html>

Electron アプリとして実行してみる

$ ./node_modules/.bin/electron .

↓実行結果です↓

ちゃんと狙い通りの 【Hello, world!】 が表示されています。

まとめ

  • ffi をインストールするときは -g, -D(--save-dev) を付けない
  • ffi をインストール後に electron-rebuild でリビルドする

おまけ

NAN の場合も同様で electron-rebuild によるリビルドが必要です。
その際、node-gyp で clean, configure を行い、
その後に electron-rebuild すれば OK です。

$ ./node_modules/.bin/node-gyp clean
$ ./node_modules/.bin/node-gyp configure
$ #./node_modules/.bin/node-gyp build // ←これをやらずに
$ ./node_modules/.bin/electron-rebuild // ←代わりにこっちを実行する