Electron で C# の DLL を使いたい場合、
- electron-edge-js を使って呼び出す。
- DLL をインポートした EXE を作成して、 Node.js の child_process.execFile を使って EXE を呼び出す。
という方法があるようで、今回は electron-edge-js を使って呼び出す方法を試した。
(サンプルソース : https://github.com/nakamurakko/electron-dll-sample)
開発環境
- Visual Studio 2022
- ワークロードは下記を選択する。
- C++ によるデスクトップ開発
- .NET デスクトップ ビルド ツール
- ワークロードは下記を選択する。
- Node.js 18.3.0
- Python 3.11.0
EXE 実行確認環境
- Windows 11 (64ビット)
- Visual C++ 再頒布可能パッケージ (64ビット)
- .NET 6.0 SDK (64ビット)
- Edge.js.CSharp が SDK を実行時に参照しているため、実行環境に SDK をインストールする必要がある。
- .NET 6.0 のダウンロード からダウンロードする。
- .NET Framework 4.6 Runtime が必要になる可能性あり。
- Edge.js.CSharp の動作を見る限り使用している可能性はあるが、 Windows 10、 Windows 11 には .NET Framework 4、 .NET Framework 4.8 がデフォルトでインストールされているため、要否を判断できない。エラーが発生する環境があれば .NET Framework 4.6 Runtime をチェックする。
デバッグ環境準備
下記バッチを実行すればデバッグできるようにした。実行するバッチとバッチの内容は下記の通り。
install-npm.bat
を実行する。バッチの内容は下記の通り。npm install -g node-gyp
を実行する。- electron-app ディレクトリーで
npm install
を実行する。 - electron-app ディレクトリーで
npm run build-electron-edge-js
を実行する。
publish-dll.bat
を実行する。バッチの内容は下記の通り。- SampleLib.dll を発行し、 electron-app\Libraries に DLL を出力する。
- electron-app\Libraries に生成された下記ファイルを、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1\runtimes\win\lib\netstandard1.3 ディレクトリーにを作成してコピーする。
- System.Diagnostics.FileVersionInfo.dll (electron-app\Libraries\refs ディレクトリーに存在。)
- System.Text.Encoding.CodePages.dll (electron-app\Libraries\refs ディレクトリーに存在。)
- Microsoft.DotNet.InternalAbstractions.dll
- electron-app\Libraries\refs に生成されたファイルを、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1 ディレクトリーにコピーする。
- electron-app\Libraries に生成されたファイルを、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1 ディレクトリーにコピーする。 (SampleLib* はコピーしないため、 CopyExcludedFiles.txt (除外一覧)に記載。)
- electron-app\Libraries に生成された下記ファイルを、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1\runtimes\win\lib\netstandard1.3 ディレクトリーにを作成してコピーする。
- EdgeJsCSharpSharedLib.dll を発行し、 electron-app\SharedLibraries に DLL を出力する。
- SharedLibraries\System.Reflection.TypeExtensions.dll を、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1 ディレクトリーにコピーする。
- SharedLibraries\Edge.js.CSharp.dll を、 electron-app\node_modules\electron-edge-js\lib\bootstrap\bin\Release\netcoreapp1.1 ディレクトリーにコピーする。
- SampleLib.dll を発行し、 electron-app\Libraries に DLL を出力する。
build-executable.bat
を実行すれば、 electron-app\dist に electron-app 1.0.0.exe を作成する。
実装説明
各プロジェクトは下記の通り実装した。
プロジェクト EdgeJsCSharpSharedLib 作成 (Edge.js.CSharp 用 DLL)
electron-edge-js の edge.func
を呼ぶ時に Edge.js.CSharp を使用しているので、 NuGet から Edge.js.CSharp.dll
を出力する専用のプロジェクトを作成する。 Edge.js.CSharp
使用する System.Reflection.TypeExtensions.dll
のバージョンの指定が古く(Edge.js.CSharp
の TargetFramework が .NET Standard 1.6 で定義されている)、 TargetFramework が新しい DLL に取り込むと下記画像のように edge.func
を実行した時にエラーになるため、専用プロジェクトにした。
Visual Studio で、 プロジェクトの新規作成 > クラスライブラリ で DLL のプロジェクトを作成する。 edge-js の説明通り、 PreserveCompilationContext
と CopyLocalLockFileAssemblies
をプロジェクトファイルの PropertyGroup
に追記する。 TargetFramework は Edge.js.CSharp に合わせて netstandard1.6(.NET Standard 1.6)にした。
<PropertyGroup> <TargetFramework>netstandard1.6</TargetFramework> <PreserveCompilationContext>true</PreserveCompilationContext> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <Copyright>@ nakamurakko</Copyright> <Authors>nakamurakko</Authors> </PropertyGroup>
NuGet パッケージは Edge.js.CSharp
を追加した。
NuGet パッケージ名 | バージョン |
---|---|
Edge.js.CSharp | 1.2.0 |
プロジェクト SampleLib 作成 (サンプル DLL)
Electron から呼びたい DLL である SampleLib を作成する。
Visual Studio で、 プロジェクトの新規作成 > クラスライブラリ で DLL のプロジェクトを作成する。 edge-js の説明通り、 PreserveCompilationContext
と CopyLocalLockFileAssemblies
をプロジェクトファイルの PropertyGroup
に追記する。
<PropertyGroup> <TargetFramework>net6.0</TargetFramework> <PreserveCompilationContext>true</PreserveCompilationContext> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <Copyright>@ nakamurakko</Copyright> <Authors>nakamurakko</Authors> </PropertyGroup>
NuGet パッケージは edge-js の説明通り、 Microsoft.NETCore.DotNetHost
、 Microsoft.NETCore.DotNetHostPolicy
を追加する。さらに electron-app の launch.json
に "EDGE_DEBUG": "1"
を設定した状態でデバッグして、
const edge = require('electron-edge-js');
をステップ実行すると不足している Nuget パッケージがエラー出力されるので、エラーが無くなるまで追加を繰り返す。
SampleLib に追加した NuGet パッケージは下記の通り。バージョンは最新ではなく、エラーに出力されたバージョンを指定した。
NuGet パッケージ名 | バージョン |
---|---|
Microsoft.CodeAnalysis | 2.8.2 |
Microsoft.CSharp | 4.5.0 |
Microsoft.DotNet.InternalAbstractions | 1.0.0 |
Microsoft.DotNet.PlatformAbstractions | 2.1.0 |
Microsoft.Extensions.DependencyModel | 2.1.0 |
Microsoft.NETCore.DotNetHost | 7.0.2 |
Microsoft.NETCore.DotNetHostPolicy | 7.0.2 |
System.Collections.NonGeneric | 4.3.0 |
System.Collections.Specialized | 4.3.0 |
System.Data.Common | 4.3.0 |
System.Xml.ReaderWriter | 4.3.1 |
System.Xml.XmlSerializer | 4.3.0 |
System.Xml.XPath.XmlDocument | 4.3.0 |
DLL 側のメソッドは How to: support for other CLR languages に書いてある通り、 Func<object,Task<object>>
の形式で用意する。(引数は object
型で定義する事になっているけど、 string
でも問題なかった。 int
などの単純な型であれば、おそらく問題なく動作するはず。)
/// <summary> /// 挨拶を返す。 /// </summary> /// <param name="value">挨拶の相手。</param> /// <returns>挨拶</returns> public async Task<object> Reply(string value) { try { return await Task.Run(() => { return $"Hello {value}."; }); } catch (Exception e) { return e.Message; } }
これを Electron 側から呼び出す。
プロジェクト electron-app 作成 (Electron)
Electron のクイック スタートを参考にプロジェクトを作成する。
Electron 用 edge-js である electron-edge-js を使用するため、下記コマンドを実行する。
npm install electron-edge-js
Electron アプリ作成に electron-builder を使用するため、下記コマンドを実行する。
npm install --save-dev electron-builder
あとは Electron のチュートリアル プロセス間通信 を参考に実装した。実装は下記の通り。
main.js
に C# の DLL を呼び出す実装を追加する。(ipcMain.handle
は ipcMain.handle でイベントをリッスンする を参照。 DLL の呼び出し方は How to: integrate C# code into Node.js code を参照。)
ipcMain.handle('greeting', /** * 挨拶を返す。 * * @param {Electron.IpcMainInvokeEvent} event イベントデータ。 * @param {string} whoIs 挨拶する相手。 * @returns {Promise<string>} */ (event, whoIs) => { return new Promise((resolve, reject) => { const dllFilePath = path.join(getLibraryPath(), 'SampleLib.dll'); // DLL のメソッドを参照する。 const dllFunction = edge.func({ assemblyFile: dllFilePath, typeName: 'SampleLib.Greeting', methodName: 'Reply' }); // DLL のメソッドを実行する。 dllFunction(whoIs, function (error, result) { if (error) { reject(error); return; } resolve(result); }); }); });
preload.js
で公開する。(プリロード経由で ipcRenderer.invoke を公開する を参照。)
contextBridge.exposeInMainWorld('sampleLib', { greeting: (whoIs) => ipcRenderer.invoke('greeting', whoIs) });
renderer.js
でpreload.js
の API を呼び出す。
document.getElementById('greeting-button') .addEventListener('click', /** * greeting-button クリック。 * * @param {MouseEvent} ev イベントデータ。 */ (ev) => { /** @type {HTMLInputElement} */ const greetingTo = document.getElementById('greeting-to'); /** @type {HTMLElement} */ const greeting = document.getElementById('greeting'); sampleLib.greeting(greetingTo.value) .then( response => { greeting.innerText = response; }, rejected => { greeting.innerText = rejected; } ); });
index.html
でrenderer.js
に渡す項目と実行するボタンを定義する。
<table> <tbody> <tr> <td> <input id="greeting-to" title="Who is" type="text" value="Everyone" /> <input id="greeting-button" type="button" value="Greeting" /> </td> <td> <span id="greeting"></span> </td> </tr> </tbody> </table>
上記、実装が完了したらデバッグ(または ビルドした EXE を実行)して、表示される事を確認する。
補足
「Runtime、 SDK をインストールしたのに、ビルドした EXE が配布環境では動かなかった」という事が発生する。
その場合、開発 PCの .nuget ディレクトリー(C:\Users\<実行中ユーザー>.nuget)にある NuGet パッケージを EXE が参照している可能性があるため、 .nuget ディレクトリーをリネームまたは移動して EXE が実行できるか確認してみると良い。実行出来なければデバッグして、エラーが無くなるまで NuGet パッケージを DLL に追加すれば解決する。
結論
(.NET Runtime ではなく).NET SDK をインストールしないと動かないので、実用的ではないかもしれない。