はじめに
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
EACM-335のI.12.6.3 Byte ordering
depends on byte ordering might not run on all platforms.
つまり、monoを利用したクロスプラットフォームを考える場合においてはリトルエンディアンとビッグエンディアンの両方を考える必要がある。ただし、ビッグエンディアンで動くmonoの利用者の母数が少ないから、実際には考慮に入らない場合が多いのだと思う。
CPUに依存するということは、アーキテクチャがビッグエンディアンの場合はビッグエンディアンでメモリ中に格納されるということである。
グーグル先生にmono endianessで検索をかけたところ、C#では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 (); } }