nakamurakko’s blog

仕事で覚えたこと、勉強したことを自分のメモ代わりに書いていこうかなと。

Electron で C# の DLL を実行する

Electron で C# の DLL を使いたい場合、

という方法があるようで、今回は 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 実行確認環境

  1. Windows 11 (64ビット)
  2. Visual C++ 再頒布可能パッケージ (64ビット)
  3. .NET 6.0 SDK (64ビット)
    • Edge.js.CSharp が SDK を実行時に参照しているため、実行環境に SDK をインストールする必要がある。
    • .NET 6.0 のダウンロード からダウンロードする。
  4. .NET Framework 4.6 Runtime が必要になる可能性あり。
    • Edge.js.CSharp の動作を見る限り使用している可能性はあるが、 Windows 10、 Windows 11 には .NET Framework 4、 .NET Framework 4.8 がデフォルトでインストールされているため、要否を判断できない。エラーが発生する環境があれば .NET Framework 4.6 Runtime をチェックする。

デバッグ環境準備

下記バッチを実行すればデバッグできるようにした。実行するバッチとバッチの内容は下記の通り。

  1. install-npm.bat を実行する。バッチの内容は下記の通り。
    • npm install -g node-gyp を実行する。
    • electron-app ディレクトリーで npm install を実行する。
    • electron-app ディレクトリーで npm run build-electron-edge-js を実行する。
  2. 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 (除外一覧)に記載。)
    • 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 ディレクトリーにコピーする。

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 の説明通り、 PreserveCompilationContextCopyLocalLockFileAssemblies をプロジェクトファイルの 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 の説明通り、 PreserveCompilationContextCopyLocalLockFileAssemblies をプロジェクトファイルの 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.DotNetHostMicrosoft.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 のチュートリアル プロセス間通信 を参考に実装した。実装は下記の通り。

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);
      });
    });
  });
contextBridge.exposeInMainWorld('sampleLib', {
  greeting: (whoIs) => ipcRenderer.invoke('greeting', whoIs)
});
  • renderer.jspreload.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.htmlrenderer.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 をインストールしないと動かないので、実用的ではないかもしれない。