なんとなく

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

monoでBigEndian対応してみた(CSJ2Kを改修してみた)

はじめに

dvgtuViewerというSecond Life用のテキストチャットを主目的としたViewerをC#で作成している。
monoを利用しRaspberry PiやPS3 linuxおよびWindowsのPCで使用している。
ただ、PlayStation 3 Linux(以下、PS3 Linux)で使用する場合、画像が表示されないということが起こっていた。

PS3 Linuxでは、下図のように画像が表示されない。

Raspberry Piでは、下図のようにきちんと画像が表示される。

dvgtuViewerではJPEG 2000で表示するためにCSJ2Kというライブラリを使用し、変換して表示している。
PS3 Linuxの場合でもきちんと画像が表示されるようにしようと思って、調査して実装してみた。

画像が表示されないワケ

使用しているJPEG 2000を変換するためのCSJ2Kというライブラリは、libopenmetaverseのライブラリに入っているのを利用している。
https://github.com/openmetaversefoundation/libopenmetaverse

codeplexにあるものが大元だと思う。
http://csj2k.codeplex.com/

画像が表示されない原因については、おそらく、エンディアンが原因なのだろうという予想を立て、調べてみた。

PS3のエンディアン

PS3は、Cell Broadband Engineと呼ばれているCPUを使っている。
中身はPPUと呼ばれるPowerPCの演算器とSPUと呼ばれるベクトル演算器が存在するが、SPUはベクトル演算用なので、OSに使用するのはPPUで、つまりPowerPCだ。
Cell Broadband Engineの詳細については、wikipedia:Cell_Broadband_Engineを参照してほしい。

wikipedia:エンディアンによるとPowerPCエンディアンはバイエンディアンと記述されていた。
Linuxでのエンディアンについての調べ方は、wikipedia:エンディアンに記述されていたのを参考に調べてみた。

PS3 linux(PowerPC)はバイエンディアンであるが、PS3 linuxでは以下のようになり、ビッグエンディアンで起動していた。

takeshich@ps3:~$ echo -n "12345" | od -t x
0000000 31323334 35000000
0000005

また、Raspberry Pi(ARM)もバイエンディアンであるがRasbianでは以下のようになり、リトルエンディアンで起動していた。

takeshich@pi3 ~ $ echo -n "12345" | od -t x
0000000 34333231 00000035
0000005

なお、エンディアンについての詳細は、wikipedia:エンディアンの項目を参照してほしい。

C#エンディアン

PS3 Linuxでは、ビッグエンディアンで動いているということは分かった。プログラムの中でどのように扱われているのかというのも問題になる。
プログラムは、monoで動いているので、C#ではどう扱うのであろうか調べてみた。

JavaVMのエンディアンがビッグエンディアンなのに対し、C#エンディアンについては、CPUに依存するようだ。
詳しくは、EACM-335のI.12.6.3 Byte orderingにおいて以下のように規定されている。

For data types larger than 1 byte, the byte ordering is dependent on the target CPU. Code that
depends on byte ordering might not run on all platforms.

EACM-335のI.12.6.3 Byte ordering

つまり、monoを利用したクロスプラットフォームを考える場合においてはリトルエンディアンとビッグエンディアンの両方を考える必要がある。ただし、ビッグエンディアンで動くmonoの利用者の母数が少ないから、実際には考慮に入らない場合が多いのだと思う。

CPUに依存するということは、アーキテクチャがビッグエンディアンの場合はビッグエンディアンでメモリ中に格納されるということである。
グーグル先生にmono endianessで検索をかけたところ、C#ではBinaryReaderやBinaryWriterを使用しているところを当たれば、いいようだ。

CSJ2Kのソースでは

では、CSJ2Kのソースを見ていくことにする。プロジェクト内においてBinaryReaderおよびBinaryWriterで検索をかけた。

検索をかけると、画像変換用のライブラリのためか、BinaryReaderを使ってリトルからビッグへの変換するものは実装されていた。BinaryWriterにも同じような実装が見られた。
しかし、そもそもの想定がアーキテクチャとしてはリトルエンディアン前提で実装されているようであった。

それがうまくいかない原因だった。

修正

C#においてエンディアンは、BitConverter.IsLittleEndianで判定することができる。
エンディアンの変更は、以下のように型のサイズ分のバイトを配列に取得して、その配列をArray.Reverseで反転し、BitConverter.ToUInt16などで戻してあげればできる。

byte[] buf = this.ReadBytes (2);
Array.Reverse (buf);
return BitConverter.ToUInt16 (buf, 0);

ただ、修正において結構ハマってしまった。なぜハマってしまったかというと
(1)
BinaryReader派生させて、BigEndianで扱う処理しているところで、
byte[] buf = this.ReadBytes (2);
とやっているところがあって、配列にはアーキテクチャに従って値が格納される点。

(2)
return base.ReadUInt16 ();としているところがあり、
こちらは、必ずリトルエンディアンと言う点。

上記の2点をきちんと捉えていなくてハマった。
仕様上、そうなっているのだからしたが従わないといけないのだけれども、一方を見てどちらもアーキテクチャに従って値が格納されるのだろうと勝手に判断してしまった。思い込みはよくない。

結局、Console.WriteLineで出力されるデータをRaspberry PiとPS3 Linuxで出力させてみて、ビッグエンディアンかリトルエンディアンかを見ながら、実装した。
実装してからドキュメントを確認して仕様ということがわかった。

当初以下のように、実装していた。
なお、_bigEndianは強制的にbigEndianとして処理させるフラグで、BitConverter.IsLittleEndianから求められた値ではない。

public override ushort ReadUInt16 ()
{
	if (BitConverter.IsLittleEndian) {
		if (_bigEndian) {
			byte[] buf = this.ReadBytes (2);
			Array.Reverse (buf);
			return BitConverter.ToUInt16 (buf, 0);
		} else {
			return base.ReadUInt16 ();
		}
	} else {
		if (_bigEndian) {
			byte[] buf = this.ReadBytes (2);
			return BitConverter.ToUInt16 (buf, 0);
			//return base.ReadUInt16 ();とはできない。。。
		}else{
			byte[] buf = this.ReadBytes (2);
			Array.Reverse (buf);
			return BitConverter.ToUInt16 (buf, 0);
		}
	}
}

勝手にbase.ReadUInt16 ();はアーキテクチャに従ったエンディアンを返すと思って実装したけど、実際の値を確認したらそうではなかった。
なぜ、BitConverter.IsLittleEndian=falseで_bigEndian=trueのときにreturn base.ReadUInt16 ();とはするのはまちがいなのだろうか?
調べてみると、BinaryReader.ReadUInt16のドキュメントに

BinaryReader reads this data type in little-endian format.

http://msdn.microsoft.com/en-us/library/system.io.binaryreader.readuint16.aspx

という記述を見つけた。

そのため、仕様を理解でき以下のように直した。

public override ushort ReadUInt16 ()
{
	if (_bigEndian) {
		byte[] buf = this.ReadBytes (2);
		if (BitConverter.IsLittleEndian) {
			Array.Reverse (buf);
		}
		return BitConverter.ToUInt16 (buf, 0);
	} else {
		//BinaryReader reads this data type in little-endian format.
		 return base.ReadUInt16 ();
	}
}

まとめ

修正したら無事JPEG 2000のデータを変換して表示できた。
見た目で、PS3 Linuxで動いているかどうかって、判断つかないな。。。
誰得感は否めないけど、勉強になったから、備忘のために記述した。

当たり前だけど、きちんと仕様を確認する。思い込みでやらないということ。
monoでクロスプラットフォームで使用したい場合は、きちんとエンディアンに対応した処理を入れること。
エンディアンに対応した処理とは、上記の修正で行ったもの。

以下にPS3 Linuxでのスクリーンショットを。