はじめに
Xamarin.AndroidのNativeライブラリについてなかなか確認する機会がなかった。AdventCalendarの季節だし、ちょうどいい機会なので手を動かしてみて、ネタにすることにした。
試してみるNativeライブラリは何にしようかと考えた時に、2012年のセカンドライフ 技術系 Advent Calendar : ATNDに書いたエントリの中で、OpenJPEG(libopenjpeg)を使ってJPEG2000の速度計測を
そのうち調べたい
LibOpenMetaverseとCSJ2KでSIMのMinimap画像を表示してみる - なんとなく
というものがあったので、今回はlibopenjpeg(libopenjpeg-dotnet)を扱うことにした。ちなみにOpenJPEG(libopenjpeg)はJPEG 2000という画像ファイルの変換ライブラリ。JPEG 2000については、wikipedia:JPEG_2000などを参照してほしい。
上記よりこのエントリは、セカンドライフ 技術系 Advent Calendar 2013 : ATNDとXamarin Advent Calendar 2013 - Qiita [キータ]のクロスポストとさせていただく。
そのため、
- セカンドライフ 技術系 Advent Calendarからの方は、libopenmetaverseのXamarin.Androidへの移植におけるNativeライブラリについてという視点
- Xamarin Advent Calendarからの方は、Xamarin.AndroidでのNativeライブラリの使用についてという視点
で見ていただきたい。
NDKでの作業
NDKの準備
Nativeライブラリを扱うに、まずは、AndroidのNDKを準備する必要がある。
WindowsのXamarin.StudioでNDKを指定するところがあるが、これはWindowsではどう使われるのだろうか。。。はて?とちょっと立ち止まった。
Using Native Libraries | Xamarinを参照しても、特に触れられてない。cygwinとかいれて汚くしたくなかったし、Windowsな環境でXamarin.StudioからNDKを使ってコンパイルする方法がよくわからなかった。詳細なドキュメントを見つけられなかったというのが正直なところだ。
そのため、LinuxでNDKの環境を作った。
とりあえず: [Android][お勉強] NDK環境構築とGetting Startedを参考にした。
Linuxにおける準備は簡単で、AndroidのNDKをダウンロードして、解凍すれば環境はできる。
$ wget http://dl.google.com/android/ndk/android-ndk-r9b-linux-x86.tar.bz2 $ tar xvfj android-ndk-r9b-linux-x86.tar.bz2
対象のソースについて
はじめにで触れたLibOpenMetaverseとCSJ2KでSIMのMinimap画像を表示してみる - なんとなくでは、libopenmetaverseというC#で書かれたmetaverse向け(SecondLifeやOpenSim)のライブラリを扱っている。libopenmetaverseの中でOpenJPEG(libopenjpeg)がプラットフォーム呼び出しで使用されている。エントリを書いた当時は、私のMono for Android*1への理解が追いついておらず、Nativeライブラリの導入にいろいろと時間がかかりそうで、調べきれてなかった。そのため、CSJ2KというC#で書かれたJPEG 2000を変換するためのライブラリを見つけて、少々改修し(CSJ2Kの一部とlibopenmetaverのJPEG2000を扱うところ)、Mono for Androidで動くようにしていた。
今回は、OpenJPEG(libopenjpeg)の純正を扱うのではなく、libopenmetaverseのXamarin.Androidへの移植を考慮し、libopenmetaverse内で使われているopenjpeg-dotnetを使用することにした。
ソースの準備
jniディレクトリを作成し、そこにhttps://github.com/openmetaversefoundation/libopenmetaverse/tree/master/openjpeg-dotnet
よりダウンロードしたlibopenjpegとdotnetのフォルダをフォルダごとコピーした。
そして、後述するAndroid.mkとApplication.mkを作成した。
~/jni/ ~/jni/libopenjpeg/ ~/jni/dotnet/ ~/jni/Android.mk ~/jni/Application.mk
Android.mkの作成
とりあえず: [Android][お勉強] NDK環境構築とGetting Startedによると、.cおよび.cppをLOCAL_SRC_FILESに列記すれば、ヘッダファイルはコンパイル時に解析して見つけてくれるとの事だったので、以下のようにAndroid.mkを作成した。
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := openjpeg-dotnet LOCAL_SRC_FILES := \ libopenjpeg/bio.c \ libopenjpeg/cio.c \ libopenjpeg/cidx_manager.c \ libopenjpeg/dwt.c \ libopenjpeg/event.c \ libopenjpeg/image.c \ libopenjpeg/j2k.c \ libopenjpeg/j2k_lib.c \ libopenjpeg/jp2.c \ libopenjpeg/jpt.c \ libopenjpeg/mct.c \ libopenjpeg/mqc.c \ libopenjpeg/openjpeg.c \ libopenjpeg/phix_manager.c \ libopenjpeg/pi.c \ libopenjpeg/ppix_manager.c \ libopenjpeg/raw.c \ libopenjpeg/t1.c \ libopenjpeg/t1_generate_luts.c \ libopenjpeg/t2.c \ libopenjpeg/tcd.c \ libopenjpeg/tgt.c \ libopenjpeg/thix_manager.c \ libopenjpeg/tpix_manager.c \ dotnet/dotnet.cpp include $(BUILD_SHARED_LIBRARY)
libopenjpeg-dotnet.soというものを作りたかったので、LOCAL_MODULEはopenjpeg-dotnetと記述した。また、純粋なlibopenjpegではないため、dotnet/dotnet.cppを加えた。dotnet.cppとdotnet.hはプラットフォーム呼び出しの際に使用しやすいように作成されたようだ。
Application.mkの作成
#include <algorithm>
としているため、Android NDKでSTL使えるようにしなくてはならないようだ。
そのためApplication.mkを作成した。
APP_PROJECT_PATH := $(call my-dir) APP_STL := stlport_static APP_BUILD_SCRIPT := Android.mk STLPORT_FORCE_REBUILD := true APP_ABI := armeabi armeabi-v7a mips x86 APP_OPTIM:=release
Android NDKでSTL使えるようにするには、
APP_STL := stlport_static
とした。
ただ、これだけでは、コンパイル時にライブラリがないと怒られてしまった。
Android NDKが標準で備えるSTLを利用する(UsefullCode.net)によると
STLPORT_FORCE_REBUILD := true
とすることによって
Android NDKに含まれるSTLのソースファイル一式が強制ビルドされてプロジェクトフォルダ内にlibstlport_static.aが生成、利用されるようだ。
Xamarin.Androidでの作業
アプリケーションへの実装
Using Native Libraries | Xamarinを参考にしようとしたが、あまり参考にならなかった。ドキュメントが更新されていないのかもしれない。CPU Architecture | Xamarinは少し参考になった。
手順は
- NDKで作成した.soをプロジェクト内にAddする。
- Addした.soのビルドアクションを変更する。
- オプションでサポートするプラットフォームにチェックを入れる。
- プラットフォーム呼び出しのコードを書く。
ここからはXamarin.Studioでの作業になる。NDKで作成されたlibsフォルダごとプロジェクトにAddする。
ただし、MIPSはXamarin.Andriodでサポートしていないので、削除しておく必要がある。もしくは前述したApplication.mkのAPP_ABI :=からMIPSを削除する。
数分気付かずにいて、デプロイできなくて、「え?何?」って状態になった。ハマるのは私だけかもしれないが^^;
そして、Addしたlibopenjpeg-dotnet.soを
- Xamarin.Android アプリケーションとして、デプロイする場合は、ビルドアクションをAndroidNativeLibraryに
- Xamarin.Android ライブラリーとして、デプロイする場合は、ビルドアクションをEmbeddedNativeLibraryに
今回の試行では、Xamarin.Android アプリケーションなので、ビルドアクションをAndroidNativeLibraryとした。
libopenmetaverseのXamarin.Androidへの移植の場合は、ビルドアクションをEmbeddedNativeLibraryにする必要がある。
そして、オプションで以下の画像のようにサポートするプラットフォームにチェックを付ける。
ソースには、C#のプラットフォーム呼び出しのコードを書く。
これが普通に使える。当たり前なんだろうけど、普通に使える。
ndktest.dll.configを使ってもできた。
<configuration> <dllmap dll="openjpeg-dotnet.dll" target="libopenjpeg-dotnet.so" /> </configuration>
プラットフォーム呼び出しのソースは以下のような感じ。
// allocate encoded buffer based on length field [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetAllocEncoded(ref MarshalledImage image); // allocate decoded buffer based on width and height fields [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetAllocDecoded(ref MarshalledImage image); // free buffers [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetFree(ref MarshalledImage image); // encode raw to jpeg2000 [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetEncode(ref MarshalledImage image, bool lossless); // decode jpeg2000 to raw [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetDecode(ref MarshalledImage image); // decode jpeg2000 to raw, get jpeg2000 file info [System.Security.SuppressUnmanagedCodeSecurity] [DllImport("openjpeg-dotnet.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool DotNetDecodeWithInfo(ref MarshalledImage image);
プラットフォーム呼び出しに関するところは変更せずに、PC版で使われているlibopenmetaverse/OpenMetaverse/Imaging/OpenJPEG.cs at master · openmetaversefoundation/libopenmetaverse · GitHub
がそのままで使えた。
ただ、libopenmetaverseのXamarin.Androidに移植する際にSystem.Drawing.Imaging.BitmapDataがないので、Bitmapのヘッダを付加してrawデータをbitmapにするということは必要だ。
試しにJPEG 2000のデータをbitmapに変換して表示させてみたアプリはこんな感じ。256x256のSecondLifeのサブアカウントのプロフィールの画像ファイル。
managedなライブラリとの速度比較
ここからは自己満足な世界だけど、今まで使っていたJPEG 2000を変換するためのCSJ2Kというmanagedライブラリとlibopenjpeg-dotnetの速度を比較したいと思う。
byte[]を受け取って、bitmapのヘッダを付加する前までの変換の部分(デコードの部分)について時間を計測してみた。libopenjpeg-dotnetについては、C#側での計測になるので厳密ではないけど、ある程度というところで。
速度比較
デバッグモードで計測した。
計測対象は、
- 実機(Ainol Novo 7 Aurora) Android4.0.3
- x86ベースのエミュレータ(Intel Atom) Android4.0.3
- ARMベースのエミュレータ(armeabi-v7a) Android4.0.3
アプリのスクリーンショットに表示されている256x256のjp2のファイル(86.7KB)を変換した。
実機
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
OpenJPEG | 368 | 284 | 245 | 345 | 254 | 270 | 370 | 281 | 278 | 337 |
CSJ2K | 2024 | 1270 | 1351 | 1294 | 1322 | 1325 | 1183 | 1304 | 1260 | 1229 |
単位はms
平均 | 一回目を除いた平均 | |
---|---|---|
OpenJPEG | 303.2 | 296.72 |
CSJ2K | 1356.2 | 1289.42 |
単位はms
- 10回の平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも4.472955145倍速い。
- 一回目の実行で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも5.5倍速い。
- 一回目を除いた平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも4.345578323倍速い。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
OpenJPEG | 35 | 35 | 35 | 35 | 39 | 35 | 35 | 32 | 35 | 33 |
CSJ2K | 248 | 126 | 128 | 123 | 130 | 134 | 127 | 132 | 121 | 128 |
単位はms
平均 | 一回目を除いた平均 | |
---|---|---|
OpenJPEG | 34.9 | 34.89 |
CSJ2K | 139.7 | 128.87 |
単位はms
- 10回の平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも4.00286533倍速い。
- 一回目の実行で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも7.085714286倍速い。
- 一回目を除いた平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも3.693608484倍速い。
ARMベースのエミュレータ
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
OpenJPEG | 461 | 517 | 610 | 476 | 555 | 521 | 503 | 519 | 522 | 454 |
CSJ2K | 2889 | 1432 | 1310 | 1259 | 1354 | 1296 | 1278 | 1359 | 1358 | 1325 |
単位はms
平均 | 一回目を除いた平均 | |
---|---|---|
OpenJPEG | 513.8 | 519.08 |
CSJ2K | 1486 | 1345.7 |
単位はms
- 10回の平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも2.892175944倍速い。
- 一回目の実行で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも6.26681128倍速い。
- 一回目を除いた平均で比較するとlibopenjpeg-dotnetは、CSJ2Kよりも2.592471295倍速い。
簡単にまとめるとlibopenjpeg-dotnetは、CSJ2Kより
- 平均で比べると2.9~4.5倍ほど速くなるようだ。
- 一回目の実行で比べると5.5~7.1倍ほど速くなるようだ。
- 一回目を除いた平均で比べると2.6~4.3倍ほど速くなるようだ。
計測結果からAOT方式とJIT方式が垣間見れる。また、libopenjpeg-dotnetを使用することによって3倍程度変換速度が速くなることがわかった。
まとめ
今回、Xamarin.AndroidでNativeライブラリを使ってみて感じたのは、
- C#のプラットフォーム呼び出しが普通に使える。ソースを変更する必要もなかった。当たり前なんだろうけど、Windows関連のものを呼び出して使用していない限り、普通に使え、その移植性が高くてびっくりした。
- そして今までは、クロスプラットフォームを意識して開発をするのであれば、すべてmanagedコードで進めたほうがいいのではないかと考えていたが、Nativeライブラリが各アーキテクチャで動くという環境であれば、無理にmanagedコードのライブラリを用いてクロスプラットフォームについて考えず、Nativeライブラリも使うべきなんだと感じた。それは、速度を計測してみて、強く感じた。
ということで、libopenmetaverseのXamarin.Androidへの移植はCSJ2Kを使用せずにNativeライブラリのlibopenjpeg-dotonetを使用していこうと思う。CSJ2Kが無駄になったということは全くない。PSMとかで使えるはず。。。誰が使うのだろう。