今回はFlatBuffersでシリアライズされたバイナリデータが、実際にどのようなルールでメモリにレイアウトされているかを解説します。
使用するデータ
前回作成したバイナリデータの、メモリレイアウトを以下に示します。
・"$00"は、バイナリデータ上の0番地を示します(10進数です)。"$"を頭につけるのは分かりやすさの為で、本来こんな記法はありません(多分)。
・1バイト単位、各バイトを10進数で列挙しています。
・FlatBuffersは常にリトルエンディアンで配置されます。"12 0 0 0"は"12"のことです。
・データ中の各所に未使用領域があります。これらはvTableや参照へのアクセス時にメモリアライメントフォールトを避けるために作られたのだと思いますが、検証していないのでひとまず無視しています。
----|Header----------------------|--------------------------------------------- $00 | 12 0 0 0 |rootテーブルの相対アドレス($00+12=$12) $04 | 0 0 |[未使用領域] ----|MonsterList vTable----------|--------------------------------------------- $06 | 6 0 |vTableのサイズ(6バイト/$06~$11) $08 | 8 0 |オブジェクトのサイズ(8バイト) $10 | 4 0 |Itemsの相対アドレス(+4) ----|MonsterList Table-----------|--------------------------------------------- $12 | 6 0 0 0 |vTableの相対アドレス($12-(+6)=$06) $16 | 4 0 0 0 |[+4]Items(相対アドレス($16+4=$20)) ----|MonsterList.Items(Vector)---|--------------------------------------------- $20 | 2 0 0 0 |Itemsの要素数(2) $24 | 36 0 0 0 |Items[0]の相対アドレス $24+36=$60 $28 | 4 0 0 0 |Items[1]の相対アドレス $28+4=$32 ----|MonsterList.items[1]--------|--------------------------------------------- $32 |240 255 255 255 |vTableへの相対アドレス($32-(-16)=$48) $36 | 0 0 |[未使用領域] $38 | 3 0 |[+6]items[1].mana $40 | 4 0 |[+8]items[1].hp $42 | 5 0 |[+10]items[1].cost $44 | 32 0 0 0 |[+12]items[1].name(相対アドレス($44+32=$76)) ----|Monster vTable--------------|--------------------------------------------- $48 | 12 0 |vTableのサイズ(12バイト/$48~$59) $50 | 16 0 |オブジェクトのサイズ(16バイト) $52 | 6 0 |manaの相対アドレス(+6) $54 | 8 0 |hpの相対アドレス(+8) $56 | 10 0 |costの相対アドレス(+10) $58 | 12 0 |nameの相対アドレス(+12) ----|MonsterList.items[0]--------|--------------------------------------------- $60 | 12 0 0 0 |vTableへの相対アドレス(60-(+12)=48番地) $64 | 0 0 |[未使用領域] $66 | 0 0 |[+6]items[0].mana $68 | 1 0 |[+8]items[0].hp $70 | 2 0 |[+10]items[0].cost $72 | 16 0 0 0 |[+12]items[0].name(相対アドレス($72+16=$88)) ----|string 'Goblin'-------------|--------------------------------------------- $76 | 6 0 0 0 |stringの文字数(6) $80 | 71 111 98 108 105 110 |'Goblin' $86 | 0 0 |[未使用領域] ----|string 'Orc'----------------|--------------------------------------------- $88 | 3 0 0 0 |stringの文字数(3) $92 | 79 114 99 |'Orc' $95 | 0 |[未使用領域]
ヘッダー
----|Header----------------------|--------------------------------------------- $00 | 12 0 0 0 |rootテーブルの相対アドレス($00+12=$12)
$00からの4バイトは、rootオブジェクト(ここではMonsterList)の相対アドレスを示しています。
rootテーブル(MonsterList)
----|MonsterList Table-----------|--------------------------------------------- $12 | 6 0 0 0 |vTableの相対アドレス($12-(+6)=$06) $16 | 4 0 0 0 |[+4]Items(相対アドレス($16+4=$20))
Headerで示された$12から8バイトがMonsterListオブジェクトになります。「MonsterListオブジェクトが8バイト」という情報は、後述するvTableを先に参照して把握します。
最初の4バイトは、MonsterListのvTableへの相対アドレスです。vTableは、そのオブジェクトの各プロパティへのジャンプテーブルが格納されています。
vTableの相対アドレスは、通常現在のアドレスからの差分になります。ここでは$12-6で$06がvTableです。
次の4バイトは、Itemsへの相対アドレスです。Itemsは要素が複数個からなるVectorなので、ここには相対アドレスが入っています(ポインタや参照と同じです)。
vTable(MonsterList)
----|MonsterList vTable----------|--------------------------------------------- $06 | 6 0 |vTableのサイズ(6バイト/$06~$11) $08 | 8 0 |オブジェクトのサイズ(8バイト) $10 | 4 0 |Itemsの相対アドレス(+4)
vTableは2バイトの組になります。
最初の2バイトはvTable自身のサイズです。ここでは6バイトに指定されていて、$06から$11までがMonsterListのvTableである事を示しています。
次の2バイトは、オブジェクト本体のサイズです。ここでは8バイトと定義されています。MonsterListオブジェクトが$12から8バイトのサイズというのはこの指定によります。
最後の2バイトは、オブジェクト本体における、Itemsのデータが配置されている場所の相対アドレスになります。$12から+4番地の$16を示しています。
MonsterList.Items[0](vector)
----|MonsterList.Items(vector)---|--------------------------------------------- $20 | 2 0 0 0 |Itemsの要素数(2) $24 | 36 0 0 0 |Items[0]の相対アドレス $24+36=$60 $28 | 4 0 0 0 |Items[1]の相対アドレス $28+4=$32
vectorは、最初の4バイトで要素数を示し、続く4バイトごとに各要素への参照が格納されます。
Monster(0)
----|Monster vTable--------------|--------------------------------------------- $48 | 12 0 |vTableのサイズ(12バイト/$48~$59) $50 | 16 0 |オブジェクトのサイズ(16バイト) $52 | 6 0 |manaの相対アドレス(+6) $54 | 8 0 |hpの相対アドレス(+8) $56 | 10 0 |costの相対アドレス(+10) $58 | 12 0 |nameの相対アドレス(+12) ----|MonsterList.items[0]--------|--------------------------------------------- $60 | 12 0 0 0 |vTableへの相対アドレス($60-(+12)=$48) $64 | 0 0 |[未使用領域] $66 | 0 0 |[+6]items[0].mana $68 | 1 0 |[+8]items[0].hp $70 | 2 0 |[+10]items[0].cost $72 | 16 0 0 0 |[+12]items[0].name(相対アドレス($72+16=$88))
MonsterについてはvTableとオブジェクトをセットで解説します。
まず、$24でItems[0]の相対アドレス($24+36=$60)が算出されます。$60にはvTableへの相対アドレスが差分で格納されています($60-(+12)=$48)。この辺りはMonsterListと同じですね。
先にvTableを見てみます。$48にvTableのサイズ(12バイト)、$50にオブジェクトのサイズ(16バイト)が格納され、$52、$54、$56、$58にはmana、hp、cost、nameの各フィールドへの(オブジェクトの先頭アドレスからの)相対アドレスが格納されています。$52が(4ではなく)6になっている事に注目してください。$64から2バイトはメモリアラインのために未使用領域になっているため、manaは先頭アドレスから6バイト目に格納されているのです。
Monsterオブジェクトの各フィールドは$64から配置されています。mana/hp/costはshort型なので2バイトですが、nameは4バイトで相対アドレスを格納します。
文字列
----|string 'Orc'----------------|--------------------------------------------- $88 | 3 0 0 0 |stringの文字数(3) $92 | 79 114 99 |'Orc' $95 | 0 |[未使用領域]
文字列が格納されている所を見てみましょう。$72+16=$88が文字列が配置されている番地です。最初の4バイトはstringの文字数です(ここでは3)。続く$92~$95の3バイトに文字列が格納されています(コードは常にutf8)。バイナリデータ自体は$95まであるのですが、この最後のバイトは未使用領域になります(3 0 0 0をアライメントに合わせる意図だと思います)。
補足:Monster(1)のvTable参照
Monsterは2要素登録されていますが、どちらもスキーマは同じです(当たり前ですが)。なので解説は省略しますが、items(1)のvTableへの相対アドレスだけわかりにくいので見ておきます。
----|MonsterList.items(1)--------|--------------------------------------------- $32 |240 255 255 255 |vTableへの相対アドレス($32-(-16)=$48)
この4バイトは、2進数に直すと以下のようになります(リトルエンディアンなので、逆順になります)
&B 11111111 11111111 11111111 11110000
符号アリ4バイト整数として見た場合、これは"-16"を示します。オブジェクト自体よりもvTableの方が後ろアドレスに配置されているので、相対アドレスがマイナスになるわけです(vTableへのアクセス時のみ、相対アドレスが加算ではなく差分になる事に注意してください)。
続かない
ブログ上でのFlatBuffersについての解説はいったんここまでとなります(間違いを見つけたら適宜補足しようと考えています)。
ここからは、技術書典用の原稿作業に進みます。ここまでの記事をベースに、FlatBuffersを使い倒すのに必要な情報をできるだけ詰め込んだ本にしたいと思っているので、読みたい方は完成を祈っていてください。