なんとなく

誰得感満載な記事が多いかも。Mono関係とLinuxのサーバ関係、レビューとか。

Xamarin.AndroidでNativeライブラリ(libopenmetaverseの中のlibopenjpeg-dotnet)を使ってみた

はじめに

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 : ATNDXamarin 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ではどう使われるのだろうか。。。はて?とちょっと立ち止まった。
f:id:takeshich:20131126140848p:plain

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の作成

dotnet/dotnet.cppにおいて

#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が生成、利用されるようだ。

ビルド

jniで

$ ndk-build

とすると
libsディレクトリにAPP_ABI :=で指定したアーキテクチャのフォルダとその下にlibopenjpeg-dotnet.soが作成された。

Xamarin.Androidでの作業

アプリケーションへの実装

Using Native Libraries | Xamarinを参考にしようとしたが、あまり参考にならなかった。ドキュメントが更新されていないのかもしれない。CPU Architecture | Xamarinは少し参考になった。

手順は

  1. NDKで作成した.soをプロジェクト内にAddする。
  2. Addした.soのビルドアクションを変更する。
  3. オプションでサポートするプラットフォームにチェックを入れる。
  4. プラットフォーム呼び出しのコードを書く。

ここからは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とした。
f:id:takeshich:20131125230701p:plain

libopenmetaverseのXamarin.Androidへの移植の場合は、ビルドアクションをEmbeddedNativeLibraryにする必要がある。

そして、オプションで以下の画像のようにサポートするプラットフォームにチェックを付ける。
f:id:takeshich:20131125230626p:plain

ソースには、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サブアカウントのプロフィールの画像ファイル。
f:id:takeshich:20131126163007p:plain

managedなライブラリとの速度比較

ここからは自己満足な世界だけど、今まで使っていたJPEG 2000を変換するためのCSJ2Kというmanagedライブラリとlibopenjpeg-dotnetの速度を比較したいと思う。
byte[]を受け取って、bitmapのヘッダを付加する前までの変換の部分(デコードの部分)について時間を計測してみた。libopenjpeg-dotnetについては、C#側での計測になるので厳密ではないけど、ある程度というところで。

速度比較

デバッグモードで計測した。
計測対象は、

アプリのスクリーンショットに表示されている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倍速い。

x86ベースのエミュレータ

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ライブラリを使ってみて感じたのは、

  1. C#のプラットフォーム呼び出しが普通に使える。ソースを変更する必要もなかった。当たり前なんだろうけど、Windows関連のものを呼び出して使用していない限り、普通に使え、その移植性が高くてびっくりした。
  2. そして今までは、クロスプラットフォームを意識して開発をするのであれば、すべてmanagedコードで進めたほうがいいのではないかと考えていたが、Nativeライブラリが各アーキテクチャで動くという環境であれば、無理にmanagedコードのライブラリを用いてクロスプラットフォームについて考えず、Nativeライブラリも使うべきなんだと感じた。それは、速度を計測してみて、強く感じた。

ということで、libopenmetaverseのXamarin.Androidへの移植はCSJ2Kを使用せずにNativeライブラリのlibopenjpeg-dotonetを使用していこうと思う。CSJ2Kが無駄になったということは全くない。PSMとかで使えるはず。。。誰が使うのだろう。

*1:当時はXamarin.AndroidではなくMono for Androidだった。