なんとなく

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

LibOpenMetaverse for Xamarin.Android の振り返り

はじめに

LibOpenMetaverse for Xamarin.Androidをブランチを切って公開はしていた*1のですが、とある事情*2でXamarin.Androidの環境では1年以上まともに動かなくてどうしようかと思っていました。ちょっと手を動かして改修しはじめてみてはたと気づきました。

「これは丁重に葬らなければならない」と。

まぁ、それ以外の理由もあるのですが。。。

それでせめてLibOpenMetaverseをXamarin.Androidでどうやって動かしたかを書き記しておかなければと思いたち、つらつらと書きていきます。

LibOpenMetaverseをXamarin.Androidで動かすために何をしたのかというと大まかに以下のことをしました。

  • ライブラリ関連

    • LibOpenMetaverseで使用しているライブラリにおいてXamarin.Android版が存在しないので、ソースを見つけて、それを動くようにしました。(C#のものとbindingされていたC++のものがありました)
  • Bitmap関連

    • 画像周りで差があるのでそれを変更しました。
  • プロジェクト自動生成ツール

  • Xamarin.Androidの仕様に耐える

また、Xamarin.Android向けライブラリ修正においてLibOpenMetaverseに限ったことではありませんが、Xamarin.Android向けのものはないけど、C#ソースコードが存在する場合、次のことをすると大抵の場合動かすことができます。あくまでもUIやハード周りが関係しないライブラリにおいてです。

ライブラリ関連

LibOpenMetaverseで使用しているライブラリにおいてXamarin.Android版が存在しない問題の解決

LibOpenMetaverseで参照している標準以外のライブラリで最終的に使用していたのは

  • XmlRpc
  • PrimMesher
  • Log4Net
  • SmartThreadPool

でした。

なお、zlib.netのライブラリも途中まで使用していましたが、標準ライブラリにある実装で対応できたため、使用しなくなりました。

上記に挙げたライブラリにはきちんとした形でのXamarin.Android対応を謳ったものはありません。

Log4NetについてはXamarinの中の人がとりあえず動くようにしたのをgithubで公開していたので、それを使用しました。しかしながらDLLのサイズが大きくNLog等でLoggerクラスを書き換えるべきかとずっと思っていましたが、手を動かすには至りませんでした。

Log4Net以外の

  • XmlRpcCS
  • PrimMesher
  • SmartThreadPool

については、Xamarin.Andriodで動くようにしてあげなければなりませんでした。

まず、 XmlRpcCSについては、プロジェクトファイルを変更する事によってビルドすることができましたが、LibOpenMetaverseで使用されているもののオリジナルのソースをどうしても見つけることができませんでしたので、sourceforgeにあったオリジナルと思われるものをgithubにあげているものをforkし、メソッドを拡張し、LibOpenMetaverse for Xamarin.Android向けのXmlRpcCSとしました。

GitHub - takeshich/XmlRpcCS at forandroid

つぎに、PrimMesherについては、LibOpenMetaverseで使用されているもののオリジナルのソースが明示されておらず、しかしながらLibOpenMetaverseのメイン開発者がforkしていたリポジトリがあり、おそらくそれらしいというものがありました。そして、PrimMesherについてはBitmapを扱うものですので後述しますが、Androidに対応したBitmapの処理に当該箇所を変更し、プロジェクトファイルを変更する事によってビルドすることができました。

GitHub - takeshich/PrimMesher at forandroid

さらに、SmartThreadPoolについては、必要な箇所にANDROIDプリプロセッサを追加し、プロジェクトファイルを変更する事によってビルドすることができました。ただ、このライブラリは使用する必要がないと思っています。

GitHub - takeshich/SmartThreadPool at forandroid

なお、上記3つのリポジトリにあるプロジェクトファイルは使用しておらず、後述するProtobuildによって生成されたプロジェクトファイルを使用し、ビルドしています。

C++ライブラリの対応

LibOpenMetaverseではopenjpegC++ライブラリがbindされ使用されています*3

Androidで使えるようにするためにはAndroid NDKを使用したコンパイルができるようにAndroid.mk,Application.mkを記述して、コンパイルしています。

C#側においては、AndroidではCPUのアーキテクチャをフォルダにライブラリのファイル名は同一のもの(例えば、libs/arm64-v8a/libopenjpeg-dotnet.so,libs/x86_64/libopenjpeg-dotnet.so)が使われるので、DLLの文字列を変更しています。

以前にまとめましたので以下を参照ください。

takeshich.hatenablog.com

記事を書いた後、廃止されたCPUアーキテクチャがあったり、gccが使われなくなったりして、変更はあったものの

libopenmetaverse/openjpeg-dotnet at forandroid · takeshich/libopenmetaverse · GitHub

にあるAndroid.mk,Application.mkを使えば、C++ライブラリの対応ができます。

Bitmap関連

C#において画像周りでは

System.Drawing
System.Drawing.Imaging

が使われるのが標準環境では一般的ですが、Androidでは、

Android.Graphics

を用いる必要があります。つまりこれはBitmapやBitmapDataがそのままではつかえないということです。

そのため、うまく通るようにソースを書き換える必要がありました。 LibOpenMetaverse for Xamarin.Androidでは、OpenMetaverse.Imaging以下にあるものが対象となりました。

  • BakeLayer.cs
  • ManagedImage.cs
  • OpenJPEG.cs
  • TGALoader.cs

対応当時は変更範囲が広いと判断して、ファイルごと変えてしまっています。

特にManagedImageとTGALoaderの対応については、当時はゴリゴリ書いてしまったのですが、別の書き方をこの記事を書いている最中にはたと思いたち試してみたら動きました。ほんの少しの変更でした。マジか。。。

このような感じです。

using System;

namespace OpenMetaverse.Imaging
{
    public class BitmapDataEx
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public int Stride { get; set; }
        public IntPtr Scan0 { get; set; }
    }
}
#if ANDROID
            Bitmap b = Bitmap.CreateBitmap(Width, Height, Bitmap.Config.Argb8888);

            BitmapDataEx bd = new BitmapDataEx();
            bd.Scan0 = b.LockPixels();
            bd.Height = b.Height;
            bd.Width = b.Width;
            bd.Stride = b.RowBytes;

            System.Runtime.InteropServices.Marshal.Copy(raw, 0, bd.Scan0, Width * Height * 4);

            b.UnlockPixels();
#else
            Bitmap b = new Bitmap(
                        Width,
                        Height,
                        PixelFormat.Format32bppArgb);

            BitmapData bd = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb);

            System.Runtime.InteropServices.Marshal.Copy(raw, 0, bd.Scan0, Width * Height * 4);

            b.UnlockBits(bd);
#endif

値を保持するだけのBitmapDataExクラスを追加して、Android.Graphics.Bitmapで得られるものを格納しているだけですけどね。なんでこれ当時思いつかなかったのでしょう。C#でポインタ使う概念をよく理解できていなかったのかもしれません。

プロジェクトの自動生成ツール

LibOpenMetaverseで使用しているライブラリにおいてXamarin.Android版が存在しない問題の解決において書きましたが、大抵のC#で書かれたものをXamarin.Android向けとして使用する場合プロジェクトファイルの変更が必要となります。

また、LibOpenMetaverseはprebuildというプロジェクトの自動生成ツールを使っていましたので、それを踏襲して、Protobuildというクロスプラットフォーム向けのプロジェクトの自動生成ツールを採用しました。

Protobuildがどういうものかについては、以前にまとめましたので以下を参照ください。

takeshich.hatenablog.com

実際、LibOpenMetaverse for Xamarin.Androidにおいては、 ソースをダウンロード後、コマンドプロンプトからrunprotobuild.cmdを実行することによって、必要なライブラリのソースをダウンロードし、ライブラリをビルド後、LibOpenMetaverse for Xamarin.Androidをビルドするという処理をしています。

Xamarin.Androidの提供するプロジェクトファイルがAndroidの更新に従っていろいろと変更され、Protobuildの開発が止まっているようで、変更に合わせて自分でプロジェクトファイルを生成する定義ファイルを更新していく作業が結構発生していました。しかし、これは仕方ないことだとは思います。

残念なことにProtobuildの開発は終わるようです*4。 MonoGameにおいて採用されていましたが、他で採用されているのは見たことがありませんでした。MonoGameにおいてもProtobuildの置き換えの議論がされているようです*5

ただ、自分としては、Protobuild以外を使う案を探せてはいないです。時代の流れなんでしょうかね。

Xamarin.Androidの仕様に耐える(今回のは無理)

Xamarin.Androidの仕様に耐えるとは、何かといえば、未実装であったり、バグであったりそういうXamarin.Android側の仕様を受け入れるということです。

LibOpenMetaverse for Xamarin.Androidについては2011年より対応をしています(当時は、Xamarin.Androidではなく、mono for Androidでした)。当初より数年に渡りMACアドレスの取得ができないことがあり、LibOpenMetaverseおいて影響のある部分があり、適当なMACアドレスを返却していました。ただ、それは軽微なもので、受け入れられるものではありました。

f:id:takeshich:20160314015119j:plain

しかしながら、今回、もう1年以上後方互換性が絶たれた状態になっていてデスクトップ環境では問題ないのにXamarin.Androidでは動かないということが発生していて、検討中のようではあるけれども、どうも仕様な雰囲気を出したコメントが有り、これはちょっとまずい方向性だなという雰囲気です。

昨年の記事でもHTTPによる非同期通信処理がうまくいかない状態であることを書きましたが、改善していません。

github.com

端的に言えば、サーバからデータが取得できないということです。同じソースを使っているのにXamarin.Androidだけできないということです。

Xamarin.Android側の実装を詳しくは調べていないのですが、httpwebrequestとそれに合わせた非同期処理とシグナル処理(AutoResetEventを使い、Setされるまで、WaitOne()で待つかたちのもの)の組み合わせでどうもうまく行かないようです。 httpwebrequest単独ですと問題なく動きます。非同期処理とシグナル処理側は検証できていません。

まさにこの古い仕組みでLibOpenMetaverseの非同期通信処理はされているのです。

CapsBase.csおよびCapsClient.csのソースを見るとわかると思います。

libopenmetaverse/CapsBase.cs at master · openmetaversefoundation/libopenmetaverse · GitHub

libopenmetaverse/CapsClient.cs at master · openmetaversefoundation/libopenmetaverse · GitHub

確かに現状async/awaitを使用した非同期処理を行うのが通常なのでしょう。そこでSystem.Net.Http.HttpClientを使用したものに書き換えてみました。HttpClientは、async/awaitを使用した非同期処理を行うのが通常のようで、書き換え作業をしていてすぐにLibOpenMetaverseのAPIを変える必要が出てきます。これではLibOpenMetaverseではなくなります。 例えば、Loginというメソッドは、LoginAsyncとなり、現状ではCallbackで受け付けているものをTaskの戻り値として扱うようになります。

変更すると通信処理部分のメソッドがLibOpenMetaverseではない何かになってしまい、LibOpenMetaverseとしては存在できない状況になります。 async/awaitはC# 5.0からの採用で現在では随分前のこととなります。しかしその時点でほぼLibOpenMetaverseの開発は継続されなくなっていましたから、仕方のないことです。

OSSとしてのLibOpenMetaverse for Xamarin.Androidであるならば、async/awaitへの変更は無理なことです。これがLibOpenMetaverse for Xamarin.Androidを葬ることを決めた一番の理由です。

最後に

現在、LibOpenMetaverseの開発は止まっており、LibOpenMetaverseをforkしたLibreMetaverseに移っているようです。これも葬ることを決めた理由の一つでもあります。

また、LibOpenMetaverse for Xamarin.Androidについては、広く知らしめていなかったのでほとんどの方がご存じないでしょうし、LibOpenMetaverseを使用したいという方もほとんどいないでしょうし、ましてやAndroidの環境で使いたいという方もほとんどいないのでしょう。

AndroidでサードのViewerはすでに存在します。モバイル環境においては思いの外、通信量もありwifiでないデータ通信での場合にはあまり利用されないでしょうし、wifiの環境を用意できるのであれば、Steam Linkのようなものを使うという方がいいのかもしれません*6

宅内であっても同様でモバイルviewerの必要性ひいてはそれの根幹をなすOSSとしてのLibOpenMetaverse for Xamarin.Androidが存在することはそれほど重要ではないと自分の中でストンと(やっと)理解できました。

すでにLibOpenMetaverse for Xamarin.Androidは変更しないことにしていますが、プライベートなリポジトリでLibOpenMetaverseをforkしたLibreMetaverseをforkしてfor Xamarin.Androidにし、個人用にいろいろと実装していくつもりでいます。LibOpenMetaverse for Xamarin.Androidについては、OSSとしてあることを主眼においていたらそれが足かせになり、なかなか前に進めない状況を自ら作り出してしまっていたので、今後はクローズドで行く予定です。

いろいろな理由があり、いつはじまったのか、はじまってないのに終わりを宣言する、そんなポエムにはなっていますが、葬ると言ってもLibOpenMetaverseを否定するわけではありません。Androidで動かすというXamarin.Android対応については誰もしていないことをできるようにするということであり、楽しいことでもあり、学ぶことも多くありましたので。