Windows ストアアプリから C言語の fopen を利用してローカルファイルを開く方法

久し振りのC言語ですが、ざざっと書き下します。とあるところで調べ始めてちょっと突っ込んだところで、本当にできるのか?ってのを調査している途中です。いや、正直に言うと目的を達する手段としては結構危ういので、まともに C# で書き直すなり別な方法を取ったほうがいいのですが、最初の目論見が達成できるのかを確認しているところ。

C/C++のライブラリをそのまま使う

とあるC言語で作ったライブラリがあるとします。デスクトップアプリならば、そのまま静的リンクをするかDLL呼び出しをすれば良い訳で、OpenCVとかその手のC++絡みのライブラリはデスクトップ側で動かします。それを.NETで包むか、iOS内でリンクさせて動かすか、JavaからJNIを通して動かすかは色々と動作環境により方法が違うのですが、Windows ストアアプリ上で動かそうとすると結構な手間です。

ひとつの方法としては、C++/CXでストアアプリを使って、C言語のライブラリをリンクさせます。C++/CXのストアアプリからは、stlenとかstd:stringとかの標準的な関数は大方呼び出せます。大方呼び出せますというのは、ファイル系だとかシステム系の関数とかは難しそうで、環境変数を呼び出すgetenv関数はありません。逆に言えば、ロジック系のものであればそのまま動くだろうという目算が立ちます。そうなると、既存のC言語ライブラリをそのまま移植できるわけで、特にLinux系のオープンソースなライブラリをそのままビルドして持ってくることも可能だろう、と思われます。

フロントエンドをC++/CXで作るのも良いのですが、巷のサンプル的にはC#が圧倒的に多いのと(逆に言えばC++/CXのサンプルは非常に少ない)、async/awaitの非同期処理が結構やりづらいので C++/CX でフルのフロントエンドは作りたくないところです。そのような場合は、C言語のライブラリをうまくマッピングするC++/CXのライブラリを作っておいて、C#側に提供させます。ロジック部分は全体の一部分になることが多いので、大方のところは C# で書き、小難しくて Linux 等から移植しなければいけないところを C++/CX でブリッジを書くという具合になるわけです。

大体これで行けそうな気がするのですが、ふと、移植ができるかな?というライブラリを眺めてみると、ファイル系のアクセスがありました。

fopen関数は扱えるのか?

最初の設定やらデータやらを読み込む場合には、C言語のライブラリではfopen関数が普通に使われます。あちこちで、fread/fwrite関数しているところを、WinRT の StorageFile に直すのは結構手間です…と言いますか、ほぼ不可能です。StorageFile クラスのほうは、非同期処理が入ってしまって、どうにもこうにもファイル読み込み部分の書き換えが必須になってしまいます。部分的に fread 関数呼び出しているところあれば、なんとかなるのですが、このあたりが混在して散らばっていしまっていると修正するのはちょっとお手上げ状態ですね。いちから作る直したほうが良いのでは?と思うくらいで、だからこそ、ストアアプリのほうの移植が進まないのかもしれません。

C# でファイルアクセスをしているとファイル名を直接扱うことは少ないので、じゃあ、C言語で使う fopen 関数が使えるのか?という疑問が発生します。ストアアプリの C#/VB のほうでは System.IO.File クラスが無くなっていて直接ファイル名を指定して任意なところにアクセスすることはできません。しかし、「任意なところ」以外はできるわけで、アプリケーションに含まれているファイル(Assetsの下とか、インストール先の適当なフォルダとか)、アプリケーションデータフォルダとかはアクセスが可能です、以前、直接 C++/CX で「任意なファイル」をオープンしようと思ったのですが、駄目だったのであっさりあきらめていたのですが、今回はもうちょっと突っ込んで調べてみました。

結論から言うと、fopen 系のファイルアクセスも C#/VB と同じように、アプリのインストールフォルダ内やアプリデータのフォルダへはアクセスができます。fget/fread 関数を呼び出してテキストなりバイナリデータを読み込むことができます。おそらく、アプリデータ領域であれば fwrite することも可能でしょう。この辺りは、もうちょっと後で調べていきます。ひとまず、いまやりたいことは、最初の設定が必要だったので、fopen/fread の組み合わせだけが必要になります。

ライブラリ内の fopen が有効に働く

目標としては、オープンソース系の C/C++ ライブラリをそのままストアアプリでリンクをすることです。

NuGet Gallery | Xamarin.Tesseract 0.2.4
http://www.nuget.org/packages/Xamarin.Tesseract/

実はちょうど調べていたときに、Xamarin 上で動く Tesseract-OCR を見つけました。折しも作り始めたばかりで、なんといいタイミングでしょう、という感じです。中のソースを見ると解るのですが、Xamarin.Android/iOS のプロジェクトのそれぞれの Tesseract のバイナリ形式のライブラリが含まれます。なるほど。Android の場合は、そのまま JNI で wrap させればよいし、iOS の場合には Objective-C から呼び出せるわけですから、それを C# から呼び出せる形式に変えればよいわけです。中身のファイルアクセス系も、Android/iOS ならばそのまま動きそうですね。

が、ストアアプリの場合は、中身のファイルアクセス系がそのまま動くかどうかはわかりません。Tesseract の場合 OCR の学習データのためにバイナリデータを読み込みます。これは Tesseract 自身のコードを見るとわけあるのですが、あちこちに fread 関数が散っているんですよね。ワンパスで読み込んでいるのか、適当に seek しているのかは不明ですが、これらを StorageFile 系に変えるのはほぼ不可能です。

となれば、ライブラリ内の fopen 関数が正常に動くのかどうか?が問題になってきます。

これも結論を言えば、自作の静的ライブラリでは動きました。Tesseract 自身はまだ試していないのでなんとも言えませんが、getenv 関数のダミーを作って環境変数を適当に操作してやれば読み込めそうです。

C# から C言語ライブラリの fopen を呼び出す

C# から C言語のライブラリは直接呼び出せないので、ところどころに相互運用が必要になります。

– C# でストアアプリのフロントエンドを書く。
– C++/CX で Windows ランタイムコンポーネントを作る。
– C言語のライブラリを書く

という具合になります。フロントエンドを C++/CX で書けば、C# のストアアプリは必要なくなりますが、先にも書いたように、C# のほうがサンプル数が圧倒的に多いので C# でフロントエンドを書くほうがよいでしょう。

また、「C言語のライブラリを書く」ところは、C++/CX のライブラリにしてもよいのですが、これだと C++/CX の文法に寄ってしまったり、コードの保守が煩雑になってしまうので、別途ビルドした C/C++ ライブラリを静的にリンクさせるほうがよいでしょう。DLL の場合はどうだったのかはわかりません。DLL 自体の配布がややこしくなるので、これは別途要検討というところですね。

プロジェクト形式は、

– CheckApp — C# のストアアプリ
– CheckCLib — C言語のライブラリ
– CheckCppcx — 相互運用の C++/CX ライブラリ

になります。ストアアプリ内にある Assets/sample.txt を fopen して読み込もうという計画です。

C言語のライブラリ(CheckCLib)は、ごく普通に fgets でファイル読み込みをします。ここではバッファは面倒なので static で返していますが、別途バッファを渡すほうが安全です。このあたりメモリ操作系の malloc/free, new/delete の扱いも注意しないといけませんね。

char *Class2::OpenPath(const char *path)
{
	FILE *fp = fopen(path, "rt");
	static char buf[256] = { 0 };
	fgets(buf, 256, fp);
	fclose(fp);
	return buf;
}

これを C++/CX のライブラリから呼び出します。C# からは、char* を直接扱えないので String から変換させます。

Platform::String^ Class1::OpenPathC(Platform::String^ fpath)
{
	std::wstring w(fpath->Data());
	std::string s(w.begin(), w.end());
	const char *path = s.c_str();

	auto obj = new Class2();
	auto buf = obj->OpenPath(path);

	std::string ss(buf);
	auto text = ref new String(std::wstring(ss.begin(), ss.end()).c_str());
	return text;
}

C++/CX では文字列を Platform::String で扱います。この String は wchar_t になっているので、std::wstring と std::string を駆使して const char * に変換します。漢字が入っているとこれでは駄目なのですが、ASCII の範疇であればこれで十分です。

この C++/CX で作ったライブラリを C# のストアアプリから呼び出すと次のコードになります。

private void clickOpen6(object sender, RoutedEventArgs e)
{
    var path = "D:\work\OCR\sample\CheckApp\CheckApp\bin\x86\Debug\AppX\Assets\sample.txt";
    var obj = new Class1();
    var text = obj.OpenPathC(path);
    text1.Text = text;
    text2.Text = path;
}

パスがいやに直接的に指定していますが、これで動きます。実はアクセスさせてみると分かりますが、AppX 以下のファイルであれば普通に fopen できますが、それ以外の場所だとエラーになります。また AppX 内であってもプロジェクト内に含まれていないファイルにアクセスしようとするとエラーになります。きっちりと、ストアアプリの StorageFile 系のアクセス禁止と同じ法則が適用されているのが素晴らしいところです。これ、OS の API レベルでガードされているということですよね。

ここのパス名をどうやってとるのかというと、普通に ms-appx:/// を使うこともできます。

private async void clickOpen6(object sender, RoutedEventArgs e)
{
    var uri = new Uri("ms-appx:///Assets/sample.txt");
    var file = await StorageFile.GetFileFromApplicationUriAsync(uri);
    var path = file.Path;
    var obj = new Class1();
    var text = obj.OpenPathC(path);
    text1.Text = text;
    text2.Text = path;
}

こんな風に、一度 StorageFile.GetFileFromApplicationUriAsync を使って呼び出して、Path プロパティで取得すればよいのです。こうすると、いつも通り、ms-appx:/// でアクセスできるので、ファイルの絶対パスが分からなくても良いのです。

サンプルコード

テスト時に作ったサンプルコードはこちら。インクルードパス等が固定になっているかもしれませんが、参考まで。

CheckApp-v0.1-src.zip

おまけ

ストアアプリでファイルアクセスをするときには、非同期の await/async が必須になります。しかし、コードを見てみると気付くと思いますが、C言語の fopen 関数の場合は同期的に作ることになります。そういう意味では、ストアアプリの基準から逸脱しているようにも見えますが、逆に言えば、デスクトップアプリと同じような仕組みでファイルアクセスをする手段を得られる、ということでもありますね。ストアアプリの場合、http アクセスしてちらちらとデータを取ってくるのには適しているのですが、ローカル環境でがりがりと動かすにはちょっと道具不足、と言いますか昔ながらの C言語アクセスが楽だったりします。多分、std::cin/cout も使えるんじゃないかなと思うので、このあたりは後で試してみたいと思います。

補記

CreateFile2 function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/hh449422(v=vs.85).aspx

When called from a Windows Store app, CreateFile2 is simplified. You can open only files or directories inside the ApplicationData.LocalFolder or Package.InstalledLocation directories. You can’t open named pipes or mailslots or create encrypted files (FILE_ATTRIBUTE_ENCRYPTED).

とあるので、内部的に Windows ストアアプリも fopen も CreateFile2 関数の動きに準じてそうとのこと Special thanks @biac -san .

カテゴリー: 開発, C#, C++/CX パーマリンク