窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

Blazor WebAssemblyアプリ上で使えるNativeFileReferenceを使ったPreBuiltなwasmを含むパッケージを作ってみる

前回のメモの続きです。

Blazor WebAssemblyアプリ上に展開されているFileSystemを触る - 窓を作っては壊していた人のブログ

前回のメモでBlazor WebAssembly上に展開されているFileSystemに対して、System.IO.File経由またwasm上からアクセス出来る旨をご紹介しました。 これを利用してFileを利用するC言語で書かれたアプリケーションをBlazor WebAssembly上で使ってみようと、また作ってみようと思います。

作ったもの

アプリケーション側

yamachu.github.io

GitHub - yamachu/SharpOpenJTalkWasmSample

Open JTalk と言う日本語テキストのTTSアプリケーションをBlazor WebAssembly上で動かしたものになります。

Initialize時にOpen JTalkで使用する辞書ファイルと音声ファイルをFileSystem上に展開し、それを使って音声合成を行っています。

ライブラリ側

github.com

Open JTalkをWindows, macOS, LinuxC# から使うためのライブラリ。 既にP/Invokeが行える様にバインディングなどのコードが含まれている。

今回はこの対象のOS/Archにwasmを追加した形になります。

作り方

ASP.NET Core updates in .NET 6 Release Candidate 2 - .NET Blog

こちらに書かれている内容に沿って、ネイティブ依存のあるライブラリを作っていきます。 Blazor WebAssemblyアプリケーション側のcsprojにNativeFileReferenceを載せてCファイルをビルドし参照する方法は書いてあることそのままなので割愛します。

ざっくり必要な作業としては

  1. Emscripten 2.0.23 で対象のCライブラリをビルドする
  2. P/Invokeを行うためのバインディングを含むコードを書く
  3. NativeFileReferenceとしてマークするためのpropsファイルを書く
  4. nugetパッケージとして固める

といった4ステップです。

実際に自分が行ったことのメモ程度ですが、手順だったり実際のコードを紹介します。

Emscripten 2.0.23 で対象のCライブラリをビルドする

LibOpenJTalk/build-library.yml at 43d970cc8db98e37d5e48e725d51a0eaef04151b · yamachu/LibOpenJTalk · GitHub

ビルドのためのGitHub Actionsはこんな感じ。 例えばconfigureでビルドの設定が可能なプロジェクトの場合はemconfigureなどを利用しemscriptenでビルドが行える様に設定し、ビルドを行います。 今回は .wasm にしないで、.a ファイルとして出力をしています(ただのビルド済みファイルのアーカイブなので取り回しが良さそうという判断から)。

詳しい手法は以下のドキュメント参照。

Building Projects — Emscripten 2.0.33-git (dev) documentation

P/Invokeを行うためのバインディングを含むコードを書く

P/Invokeとはという方は公式ドキュメントを参照。

プラットフォーム呼び出し (P/Invoke) | Microsoft Docs

実際のバインディングのコードはこの辺り

SharpOpenJTalk/CoreDefinitions.cs at f864409201d446de8757a8b2d4fdde17e5c29dc8 · yamachu/SharpOpenJTalk · GitHub

昔作ったライブラリではどの様に作っていたかを書いていたようなので、詳しくはこちらをご覧ください。

teitoku-window.hatenablog.com

最近では

github.com

を使ってバインディングのコードを作ることも行われているようです(まだ未検証)。

NativeFileReferenceとしてマークするためのpropsファイルを書く

Blazor WebAssemblyでビルドを行う際にNativeFileReferenceとしてNative依存のファイルを読ませたいので、nugetパッケージの build/net6.0 以下にpropsファイルを配置します。 今回はTFMがnet6.0なBlazor WebAssemblyなのでbuild/以下はnet6.0を指定しています。 なぜ build/ 以下なのかは公式ドキュメントを(略

nuget.exe CLI を使用して NuGet パッケージを作成する | Microsoft Docs

実際のnugetパッケージの中身はこんな感じの形になっています。

# ~/.nuget/packages/sharpopenjtalk/1.4.0 $ tree
.
├── Content
├── build
│   └── net6.0
│       └── SharpOpenJTalk.props
├── lib
│   └── netstandard1.3
│       ├── SharpOpenJTalk.dll
│       └── SharpOpenJTalk.pdb
├── runtimes
│   ├── browser-wasm
│   │   └── 2.0.23
│   │       └── openjtalk.a
│   ├── linux
│   │   └── native
│   │       └── libopenjtalk.so
│   ├── osx
│   │   └── native
│   │       └── libopenjtalk.dylib
│   ├── win-x64
│   │   └── native
│   │       └── openjtalk.dll
│   └── win-x86
│       └── native
│           └── openjtalk.dll
├── sharpopenjtalk.1.4.0.nupkg
├── sharpopenjtalk.1.4.0.nupkg.sha512
└── sharpopenjtalk.nuspec

この SharpOpenJTalk.props に

<?xml version="1.0"  encoding="utf-8"?>
<Project>
  <ItemGroup>
    <NativeFileReference Include="$(MSBuildThisFileDirectory)..\..\runtimes\browser-wasm\2.0.23\openjtalk.a" />
  </ItemGroup>
</Project>

こんな感じのXMLを記述し、2.0.23なEmscriptenでビルドしたArchiveファイルを参照させるようにしています。 こうすることでBlazor WebAssemblyでこのパッケージを追加した際に openjtaljk.a が NativeFileReference として認識され、dotnet.wasm に含まれるようになります。

nugetパッケージとして固める

上記のツリーのような構成になるようにnuspecを書いてパッケージングしましょう。

ざっくりですが、上記の手順でパッケージを作ることで、Blazor WebAssemblyアプリケーション上で動作するNative依存を含むライブラリが作成できます。

ハマりどころ

別のラッパーライブラリ では Archive ファイルを browser-wasm/native 以下に配置してしまったため、publishした際、publishディレクトリに Archive ファイルが含まれてしまいました。

公式ブログには

The files for the native dependencies should be built for WebAssembly and packaged in the browser-wasm architecture-specific folder.

と書かれていたのでてっきり native 以下なのかなと判断し配置しましたが、browser-wasm 以下であればどこでも良さそうです。

またNative依存が正しく呼び出せるかどうかなどの確認は対象OSをWindows, macOS, Linuxにして、Consoleアプリケーションなどで確認する方が良いです。 呼び出しに失敗している、引数が合わないなどはBlazor WebAssemblyアプリのデバッグでは非常に困難なためです。

Blazor WebAssemblyアプリ上に展開されているFileSystemを触る

.NET 6 RC2でのお話です。 触ってみたやつをメモっておく。

Blazor WebAssemblyを支える技術であるEmscriptenEmscriptenは仮想のファイルシステムをサポートしており、JSやwasmから読み書きが行えるようになっています。

emscripten.org

さて、このBlazor WebAssemblyもこのEmscriptenを使っているのだし、C#から読み書きできるの?と気になったので試してみました。

結論としては出来たので、使ったコード片を残しておきます。

@using System.IO

<button onclick="@LS">lsするぞ</button>
<input type="text" @bind-value="dir" @bind-value:event="oninput" />

...

@code
{
    string dir = "";

    void LS()
    {
        LSImpl(dir);
    }

    void LSImpl(string rootDir)
    {
        try
        {
            foreach (var f in Directory.GetFiles(rootDir))
            {
                Console.WriteLine(f);
            }
            foreach (string d in Directory.GetDirectories(rootDir))
            {
                Console.WriteLine(d);
                LSImpl(d);
            }
        }
        catch (Exception e)
        {
            // System.UnauthorizedAccessException: Access to the path '/proc/self/fd' is denied.
            Console.Error.WriteLine(e);
        }
    }
}

こんな感じのコードを流し込むと

/
/tmp
/home
/home/web_user
/dev
/dev/null
/dev/tty
/dev/tty1
/dev/random
/dev/urandom
/dev/stdin
/dev/stdout
/dev/stderr
/dev/shm
/dev/shm/tmp
/proc
/proc/self
/usr
/usr/share
...

こんな感じで仮想ファイルシステムの中身が列挙されます。

BlazorWebassembly(.NET6 rc2)のFileSystemを眺めたやつ · GitHub

この中身はemscriptenのコードに書いてあるものと同じですね。

emscripten/library_fs.js at 2.0.23 · emscripten-core/emscripten · GitHub

これでC#上からFileSystemにアクセスできることがわかりました。 さて気になるのは、ASP.NET Core updates in .NET 6 Release Candidate 2のアナウンスで出てきた「Native dependencies support for Blazor WebAssembly apps」で突っ込んだwasmから読み書きは?というところだと思います。

ASP.NET Core updates in .NET 6 Release Candidate 2 - .NET Blog

結論から言うと同様のFileSystemに読み書きできるので、ほんと何でも動かせるなみたいな状況になっています。

次の記事ではOpenJTalkをBlazor WebAssembly + Native dependenciesで動作させたことについてメモを書こうかと思います。