C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

react-electron-ffi-(native dll/so/dylib) プロジェクトを作るメモ (2020-03-19版)

(1/2) react-electron

ここは今回のメモの本編ではないものの、"やり方"がころころ変わるようなので一応現時点での方法をついで程度に整理します。

(1) プロジェクトのディレクトリー(=リポジトリー)を create-react-app で生成:

npx create-react-app myapp
cd myapp

(2) electron 系のパッケージを追加:

yarn add electron electron-builder wait-on concurrently --dev
yarn add electron-is-dev

(3) public/electron.js を作る:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

const path = require('path');
const isDev = require('electron-is-dev');

let mainWindow;

function createWindow() {
  app.allowRendererProcessReuse = true;
  mainWindow = new BrowserWindow({width: 1920/4, height: 1080/4, webPreferences:{ nodeIntegration: true } });
  mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`);
  mainWindow.on('closed', () => mainWindow = null);
}

app.on('ready', createWindow);

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

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

(4) package.json に electron 用のお約束を追記:

(以下のコードはmergeする追記分のみ)

{ "main": "public/electron.js"
, "scripts":
  { "dev": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\""
  }
}

ここまでの時点で yarn dev で package.json に追記した scripts の dev を呼ぶと一応 react-scripts を介して react-electron が期待動作してくれるはず。しなかったらこのメモの作成時点とは何か変化が必要にどこかが変わっている可能性があります。 Node.js, electron, Chromium など要素技術の変化はかなり激しいので、そういう事があっても慌てず問題を探りましょう、未来のわたしへ。

(5) rescripts を導入し eject せず create-react-app を扱える様に調整:

(5-1) rescripts を install

yarn add @rescripts/cli @rescripts/rescript-env --dev

(5-2) package.json / scripts の start, build, testreact-scripts から resccripts へ変更:

(以下のコードは該当部分の変更後のみ)

{ "scripts":
  { "start": "rescripts start"
  , "build": "rescripts build"
  , "test": "rescripts test"
  }
}

(5-3) package.json / scripts に postinstall, preelectron-pack, electron-pack を追加:

(以下のコードはmergeする追記分のみ)

{ "scripts":
  { "postinstall": "electron-builder install-app-deps"
  , "preelectron-pack": "yarn build"
  , "electron-pack": "electron-builder build -w"
  }
}

ここで electron-packelectron-builder build -w-wWindows 向けのパッケージングを有効にする引数。OSX向けを有効にしたければ -mGNU/Linux 向けを有効にしたければ -lWindowsOSXGNU/Linux 向けを同時に有効にしたければ -wml のように定義でき、プロジェクトに応じて変更します。必要ならプラットフォームのアーキテクチャーも --ia32, --x64 で設定できます。

(5-4) package.json / scripts から eject を削除

rescripts を使う場合 eject を使う必要は無いし、残しておいても事故の元になるだけなので scripts から完全に消し去ります。

(5-5) package.json に rescripts を追記:

(以下のコードはmergeする追記分のみ)

{ "rescripts": [ "env" ]
}

これを書かないと electron のウィンドウ(=client-side)は動いても react な中身(=server-side)が期待動作しなくなります。

(5-6) .webpack.config.js を追加:

module.exports = config =>
{
  config.target = 'electron-renderer';
  return config;
}

(5-7) .rescripts.js を追加:

module.exports = [require.resolve('./.webpack.config.js')]

(6) package.json にパッケージング用のメタデータを追加:

{ "author":
  { "name": "Author Name"
  , "email": "aurhot.email@example.com"
  , "url": "https://author-website.example.com"
  }
, "build":
  { "appId": "com.my-website.my-app"
  , "productName": "MyApp"
  , "copyright": "Copyright © 2020 ${author}",
  , "mac":
    { "category": "public.app-category.utilities"
    }
  , "files":
    [ "build/**/*"
    , "node_modules/**/*"
  ]
, "directories":
  { "buildResources": "assets"
  }
}

(7) assets / icon.png を作成

  1. assets ディレクトリーを作成
  2. 256x256 の icon.png を放り込む(アプリのアイコンになります)

以上で react-electron の 2020-03-19 頃の準備手順は完了です。 yarn dev で開発版を実行、 yarn electron-pack でリリース版パッケージを dist ディレクトリーへ生成できるようになっています。なっていなければ、このメモの作成時点とは何か変化が必要にどこかが変わっている可能性があります。 Node.js, electron, Chromium など要素技術の変化はかなり激しいので、そういう事があっても慌てず問題を探りましょう、未来のわたしへ。

動作を確認したら次の react-electron を react-electron-ffi-(native dll/so/dylib) へ進みましょう。

(2/2) react-electron から react-electron-ffi-(native dll/so/dylib) へ

react-electron を使わない Node.js で FFI して native dll/so/dylib を呼ぶ方法は1つ前回書いた Node.js と FFI の 2020-03-18 時点でのメモ; node-ffi 系 → node-ffi-napi 系 - C++ ときどき ごはん、わりとてぃーぶれいく☆ のように node-ffi 系ではなく node-ffi-napi 系を使う事だけ知っていればわりと簡単に yarn add ffi-napi ref-napi して、

var ffi = require('ffi-napi')
var ref = require('ref-napi')
var s = ref.types.CString
var nyanko = ffi.Library( 'nyanko.dll',{ 'f': [ s, [] ] } )
console.log( nyanko.f() )

のように使えばよいだけです。これを踏まえて、 (1/2) で作成した react-electron のプロジェクトで、

  1. yarn add ffi-napi ref-napi して
  2. src/App.js で前述のような FFI コードを記述して実行

すると:

f:id:USAGI-WRP:20200318225023p:plain

こんな具合で死にます。☠ Node.js, react, electron に慣れていない初心者にはわけがわからない上にググらビリティーに問題を抱えたエラーの様に感じられますが、わかってしまえばどうという事はなくなります:

(1) public/electron.js の new BrowserWindowwebPreferences.nodeIntegration を明示的に true に変更する:

  mainWindow = new BrowserWindow({width: 1920/4, height: 1080/4, webPreferences:{ nodeIntegration: false } });

これをやらないと次の(2)だけやっても "window.require is not a function" とか electron のウィンドウにエラーが表示される事になります。nodeIntegrationtrue にしないと Node.js 部分の機能が有効にならないため、 fs も使えないとかそういう事が起こります。そうなると ffi も使えません。(2/2) の最初の死亡状態で無いと言われていた exists はたぶん fsexists でしょう。そこに気がつければこの問題と原因と解決方法へ繋がります。

(2) src/App.js では window.require('electron').remoterequire して FFI する

const remote = window.require('electron').remote;
const ffi = remote.require( 'ffi-napi' );
const ref = remote.require( 'ref-napi' );

こんな具合で期待動作します。これで例えば:

let s = ref.types.CString;
let nyanko = ffi.Library( '../fuga/nyanko.dll',{ 'f': [ s, [] ] } ); // 例として前回適当に作った文字列値を返すだけのライブラリーを束縛
let mewing = nyanko.f();

としておいて、 function App()return{mewing} を:

          Learn React {mewing} !

など仕込んで実行すると:

f:id:USAGI-WRP:20200319013223p:plain

こうなります。成功👍

もし、 yarn dev する場合とパッケージングして実行する場合では native dll/so/dylib のパスが変わる場合は:

const is_dev = remote.require( 'electron-is-dev' );
const nyanko_path = is_dev ? '../fuga/nyanko.dll' : 'nyanko.dll';
let nyanko = ffi.Library( nyanko_path,{ 'f': [ s, [] ] } )

と electron-is-dev を使うと簡単です。呼び出す native dll/so/dylib もオリジナルに作ったものを使用したいプロジェクトではこのように対応します。あるいは、たぶん実行時の引数かシェル変数で渡す事にしておいて dev では package.json の scripts/start で特殊化しても対応できそうですが、私は今回 electron-is-dev で満足したのでその方法については思いついただけに留まります。

ついで、蛇足となりますが自分用メモとして、 electron-is-dev の値は Boolean なので、これを文字列で表示したい場合は {is_dev} ではなく {is_dev.toString()} しないと文字列では見れません。このメモを書いた時に一度「あれ、文字列には…」と期待動作しない状態を見てしまったので自分用備忘録。

参考