先だって投稿しました
WindowsホストからWSLクライアントへ動画配信(FlatBuffersシリアライズ)
において、FlatBuffersの仕組みを取り入れて
- スキーマファイル(.fbs): シリアライズするデータの構造定義
を該当するC++言語のヘッダファイルに変換をしているのですが、本稿ではスキーマをC++ヘッダに変換する際、どのような定義に置き換えられているか説明します。
※変換前のスキーマ・返還後のC++ヘッダファイルとその他のソースコード・ビルドスクリプトは以下をご参照ください。
- GitHub
FlatBuffers スキーマ → C++ ヘッダ 変換
TCPホストからTCPクライアントにデコード済の動画を配信するプログラム実装において、まずは
- スキーマファイル
video_frame.fbsでデータの定義を記述する - コンパイラ
flatcにてC++各コードで使用されるヘッダファイルvideo_frame_generated.hを生成
する手順を踏みます。
win_wsl_flatbuffers/
├── common/
│ ├── shared_protocol.h # 動画送受信両方で使用するユーティリティを別で定義しておく
│ ├── video_frame.fbs # あらかじめデータ構造を定義(FlatBuffersスキーマ)
│ └── video_frame_generated.h # flatcでコンパイル→自動生成(v25.12.19 で生成)
(以下略)
※flatcコンパイラでのスキーマ→ヘッダファイル生成の実行コマンドは、
GiuHubに格納している windows_host/CMakeList.txt , wsl_client/CMakeList.txt をご参照ください。
1. 全体構造
VideoProto
スキーマで記述したデータ構造を含む名前空間 `VideoProto' を定義します。
// 名前空間の定義
namespace VideoProto;
// ここから名前空間に属するデータ構造の定義
// ピクセルフォーマット
enum PixelFormat : uint8 {
RGBA8 = 0,
BGRA8 = 1,
YUV420P = 2,
NV12 = 3,
}
// 転送コマンド
enum Command : uint8 {
GetFrame = 0,
GetInfo = 1,
Ping = 2,
Quit = 3,
}
// フレームメタデータ
table FrameMeta {
frame_index: uint64;
timestamp_us: uint64;
width: uint32;
height: uint32;
stride: uint32;
pixel_format: PixelFormat = RGBA8;
fps_target: float64;
encode_time_us: uint64; // エンコード所要時間
data_size: uint64; // ペイロードサイズ
}
// メインのフレームテーブル
// pixel_data は [ubyte] でゼロコピーアクセス可
table VideoFrame {
meta: FrameMeta (required);
pixel_data: [ubyte] (required); // RGBA/YUVピクセルデータ
}
// コマンドリクエスト
table Request {
cmd: Command;
request_id: uint32;
param: uint32;
}
// コマンドレスポンス(メタ情報のみ返す場合)
table InfoResponse {
request_id: uint32;
width: uint32;
height: uint32;
pixel_format: PixelFormat;
fps_target: float64;
server_version:string;
}
// 名前空間に属するデータ構造の定義ここまで
enum の変換
スキーマの定義に対してヘッダでは以下の通り変換されます。
- PixelFormat
# データ定義の変換
[.fbs スキーマ] [生成C++ヘッダ]
PixelFormat : uint8 ----flatc---> PixelFormat : uint8_t
RGBA8 = 0, PixelFormat_RGBA8 = 0,
BGRA8 = 1, PixelFormat_BGRA8 = 1,
YUV420P = 2, PixelFormat_YUV420P = 2,
NV12 = 3, PixelFormat_NV12 = 3,
PixelFormat_MIN = PixelFormat_RGBA8, ← flatcで自動生成
PixelFormat_MAX = PixelFormat_NV12 ←同上
# 定義されたデータの補助関数がヘッダ内で自動生成
[生成C++ヘッダ]
■inline const PixelFormat (&EnumValuesPixelFormat())[4]
- 全列挙値の静的配列を返す。
■inline const char * const *EnumNamesPixelFormat()
- 値名の文字列配列 {"RGBA8","BGRA8",...,nullptr} を返す。
※列挙値の命名はヘッダ関数内で以下の通り定義
static const char * const names[5] = {
"RGBA8",
"BGRA8",
"YUV420P",
"NV12",
nullptr
};
■inline const char *EnumNamePixelFormat(PixelFormat e)
- 列挙値 → 文字列へ変換。範囲外なら "" を返す。
- Command
[.fbs スキーマ] [生成C++ヘッダ]
Command : uint8 ----flatc---> Command : uint8_t
GetFrame = 0 Command_GetFrame = 0
GetInfo = 1 Command_GetInfo = 1
Ping = 2 Command_Ping = 2
Quit = 3 Command_Quit = 3
Command_MIN / Command_MAX ← flatcで自動生成する定義
# 定義されたデータの補助関数がヘッダ内で自動生成
■inline const char *EnumNameCommand(Command e)
■inline const char * const *EnumNamesCommand()
■inline const char *EnumNameCommand(Command e)
※各処理内容はPixelFormatの補助関数と同じ
table の変換
スキーマでの table 定義に対してヘッダでは struct (クラス)に変換されます。
- VTable
FlatBuffersでは VTable(Virtual Table) というテーブル内の各フィールドがバイナリバッファのどの位置にあるかを記録したオフセットテーブルが存在します。
これはFlatBuffersがゼロコピーでフィールドアクセスするための仕組みなります。
VTableのフィールドオフセットを格納するテーブルのindexは 4 から開始し 2 刻みで次のindexに移動します。
※オフセット 0 = VTableサイズ, オフセット 2 = テーブル本体サイズに使用されています。
- FrameMeta
# .fbs スキーマでのtable FrameMeta定義
[.fbs スキーマ]
table FrameMeta { ... }
|
| [生成C++ヘッダ]
+---flatc---> struct FrameMeta FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table
# ※各フィールドの型はヘッダでは以下の通り変換される
[.fbs] --flatc-> [C++ヘッダ]
uint64 uint64_t
uint32 uint32_t
PixelFormat uint8_t → static_cast<PixelFormat>
float64 double`
# 定義されたtableに対してヘッダ内に各クラスとヘルパー関数が自動生成
(1) 読み取りクラス
struct FrameMeta FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table
├── VTable オフセット定数 (enum: VT_WIDTH = 8 等)
| VTableは配列の2バイトごとに1つのフィールドデータのオフセットサイズが
| 格納されており、VT_*はフィールドに対応する配列のindexかを指す値となる
|
├── 各フィールドへのアクセサ(シリアライズ済のバッファに格納される値の読み込み)
| (例)uint32_t width() const {
| return GetField<uint32_t>(VT_WIDTH, 0);
| }
|
├── mutate関数
| シリアライズ済みのバッファ内にある フィールド の値を直接書き換え
| (例)bool mutate_width(uint32_t _width = 0) {
| return SetField<uint32_t>(VT_WIDTH, _width, 0);
| }
|
└── Verify関数
シリアライズ済みのバッファ内の整合性を検証する
(2) Builder クラス
struct FrameMetaBuilder
├── add_*(): 各フィールドを(frame_index、timestamp_us他)FlatBufferBuilder に追加
| (例)void add_width(uint32_t width) {
| fbb_.AddElement<uint32_t>(FrameMeta::VT_WIDTH, width, 0);
| }
├── コンストラクタ関数内でStartTable()をコールしオブジェクト生成
└── ::flatbuffers::Offset<FrameMeta> Finish() がデストラクタ関数
(3) Create ヘルパー関数
CreateFrameMeta(fbb, ...) I/F関数(全引数に各フィールドを用意し一括で指定)
VideoFrame
root_type VideoFrame:
スキーマファイルの最後に
root_type VideoFrame;
と ルートタイプ 定義することにより、バッファ全体のルートとして VideoFrame を扱うためのクラスと関数群がヘッダに生成されます。
(1) VideoFrameクラス
[.fbs スキーマ]
root_type VideoFrame:
|
| [生成C++ヘッダ]
+---flatc---> struct VideoFrame FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table
(2) 自動生成する関数群
■ `GetVideoFrame(const void *buf)`
バッファ先頭から `VideoFrame` の読み取り専用ポインタを返す。受信側のエントリポイント
■ `GetSizePrefixedVideoFrame(buf)`
サイズプレフィックス付きバッファに対応
■ `GetMutableVideoFrame(void *buf)`
書き換え可能なポインタを返す(mutate系関数と組み合わせて使う)
■ `GetMutableSizePrefixedVideoFrame(buf)`
上記のサイズプレフィックス対応
■ `VerifyVideoFrameBuffer(verifier)`
バッファ全体のValidationを実行。不正バッファの検出
■ `VerifySizePrefixedVideoFrameBuffer(verifier)`
上記のサイズプレフィックス対応
■ `FinishVideoFrameBuffer(fbb, root)`
ビルダーにルートオフセットを設定してシリアライズを完了する |
■ `FinishSizePrefixedVideoFrameBuffer(fbb, root)`
サイズプレフィックス付きの対応
(3) Builderクラス
struct VideoFrameBuilder
├── add_*(): 各フィールドをVideoFrameBuilderに追加
├── コンストラクタ関数内でStartTable()をコールしオブジェクト生成
└── ::flatbuffers::Offset<FrameMeta> Finish() がデストラクタ関数
(4) Create ヘルパー関数
■CreateVideoFrame(fbb, meta_offset, pixel_data_offset) I/F関数
事前に作成済みの Offset を受け取る
■CreateVideoFrameDirect(fbb, meta_offset, &std::vector<uint8_t>) I/F関数
内部で fbb.CreateVector() を呼び、生データから直接構築
バッファ全体のルートとして VideoFrame を定義することにより、ルート内あるテーブルやフィールドに対してポインタアクセスが実行できるようヘッダ内に関数が生成されます。
┌-------------------------------------┐ ┌-------------------------------------------------┐
.fbs スキーマ 生成C++ヘッダ内定義
├-------------------------------------┤ ├-------------------------------------------------┤
table VideoFrame { // ネストされたテーブルに対しポインタアクセス
meta: FrameMeta ---flatc--> const FrameMeta* meta() const;
(required); FrameMeta* mutable_meta();
pixel_data: [ubyte] // [ubyte] → Vector<uint8_t> ポインタアクセス
(required); const Vector<uint8_t>* pixel_data() const;
} Vector<uint8_t>* mutable_pixel_data();
└-------------------------------------┘ └-------------------------------------------------┘
Request(コマンドリクエスト)
以下のようにフィールド型の変換をおこないます。
# .fbs スキーマでのtable Request定義
(1) Request クラス
[.fbs スキーマ]
table Request { ... }
|
| [生成C++ヘッダ]
+---flatc---> struct Request FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table
[.fbs スキーマ] [生成C++ヘッダ]
┌--------------------------------┐ ┌-------------------------------------------┐
table Request {
cmd: Command; --flatc--> Command cmd() // uint8_t→cast
request_id: uint32; uint32_t request_id() // GetField直接
param: uint32; uint32_t param() // GetField直接
}
+ 各 mutate_*() 関数
+ CreateRequest(fbb, cmd, id, param)
└--------------------------------┘ └-------------------------------------------┘
(2) Builder クラス
struct RequestBuilder
├── add_*(): 各フィールドをRequestBuilder に追加
├── コンストラクタ関数内でStartTable()をコールしオブジェクト生成
└── ::flatbuffers::Offset<Request> Finish() がデストラクタ関数
(3) Create ヘルパー関数
CreateRequest(fbb, ...) I/F関数(全引数に各フィールドを用意し一括で指定)
InfoResponse(string 型の変換)
上記と同様にフィールド型の変換をおこないます。
# .fbs スキーマでのtable InfoResponse定義
(1) Request クラス
[.fbs スキーマ]
table InfoResponse { ... }
|
| [生成C++ヘッダ]
+---flatc---> struct InfoResponse FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table
[.fbs スキーマ] [生成C++ヘッダ]
┌--------------------------------┐ ┌----------------------------------------------┐
table InfoResponse {
request_id: uint32; uint32_t request_id()
width: uint32; --flatc--> uint32_t width()
height: uint32; uint32_t height()
pixel_format: PixelFormat; PixelFormat pixel_format() // uint8_t→cast
fps_target: float64; double fps_target()
server_version:string; const String* server_version() con
} // string → flatbuffers::String*
└--------------------------------┘
+ String* mutable_server_version();
└----------------------------------------------┘
(2) Builder クラス
struct InfoResponseBuilder
├── add_*(): 各フィールドをInfoResponseBuilder に追加
├── コンストラクタ関数内でStartTable()をコールしオブジェクト生成
└── ::flatbuffers::Offset<Request> Finish() がデストラクタ関数
(3) Create ヘルパー関数
■CreateInfoResponse(fbb, ...) I/F関数
事前に fbb.CreateString() した Offset を渡す
■CreateVideoFrameDirect(fbb, meta_offset, &std::vector<uint8_t>) I/F関数
└─ const char* を渡すと内部で CreateString() を呼んでくれる
└─ nullptr を渡すとフィールド省略
TypeTable(ミニリフレクション)
TypeTableは、スキーマの構造情報(フィールド名、型、ネスト関係など)を実行時にプログラムから参照できるようにしたメタデータテーブルです。
TypeTableは主にデバッグや汎用ツール向けに用意された機能となります。
各型に *TypeTable() 関数が生成され、実行時にスキーマのメタ情報を参照を可能とします。
■FrameMetaTypeTable() → ST_TABLE, 9フィールド
■VideoFrameTypeTable() → ST_TABLE, 2フィールド
■RequestTypeTable() → ST_TABLE, 3フィールド
■InfoResponseTypeTable() → ST_TABLE, 6フィールド
■PixelFormatTypeTable() → ST_ENUM, 4値
■CommandTypeTable() → ST_ENUM, 4値
TypeCode
FlatBuffersではTypeCodeというTypeTable内の各フィールドの型情報を3つの値でコンパクトに表現した構造体を定義しています。
ここではFlatBuffersのバージョン25.12.19に相当する構造体定義です。
struct TypeCode {
uint8_t base_type; // 基本型(ET_UINT, ET_ULONG 等)
uint8_t is_repeating; // ベクタ(配列)かどうか(0=スカラー, 1=配列)
int8_t sequence_ref; // type_refs へのインデックス(-1 = 参照なし)
};
type_refs配列は生成C++ヘッダにて以下のように定義されています。
以下FrameMetaTypeTableでの例です。
inline const ::flatbuffers::TypeTable *FrameMetaTypeTable() {
static const ::flatbuffers::TypeCode type_codes[] = {
{ ::flatbuffers::ET_ULONG, 0, -1 },
{ ::flatbuffers::ET_ULONG, 0, -1 },
{ ::flatbuffers::ET_UINT, 0, -1 },
{ ::flatbuffers::ET_UINT, 0, -1 },
{ ::flatbuffers::ET_UINT, 0, -1 },
{ ::flatbuffers::ET_UCHAR, 0, 0 }, // <-- type_refs へのインデックス sequence_ref が「0」
{ ::flatbuffers::ET_DOUBLE, 0, -1 },
{ ::flatbuffers::ET_ULONG, 0, -1 },
{ ::flatbuffers::ET_ULONG, 0, -1 }
};
static const ::flatbuffers::TypeFunction type_refs[] = { // <-- type_refs
VideoProto::PixelFormatTypeTable
};
static const char * const names[] = {
"frame_index",
"timestamp_us",
"width",
"height",
"stride",
"pixel_format",
"fps_target",
"encode_time_us",
"data_size"
};
static const ::flatbuffers::TypeTable tt = {
::flatbuffers::ST_TABLE, 9, type_codes, type_refs, nullptr, nullptr, names
};
return &tt;
}
上記ではtype_refsがVideoProto::PixelFormatTypeTableの配列を持ちtype_refs へのインデックスが「0」となっていることから、type_refs[0]はTypeTableの構造体ポインタであるVideoProto::PixelFormatTypeTableを参照しtype_refs[0]に含まれるTypeTable構造体のフィールドを参照することになります。
FrameMetaTypeTable
│
│ "pixel_format の型"
│ → type_codes[5].sequence_ref = 0
│ → type_refs[0] = PixelFormatTypeTable
│
↓
PixelFormatTypeTable が返す TypeTable
TypeTable構造体変数フィールド値を参照:
st = ST_ENUM → enum
num_elems = 4 → enumに4つの要素がある
names = [...] → 各enum値に命名されている名前
他、以下のようにtype_refs[]が返すTypeTableのポインタが自分自身を指している自己参照の場合もあります。
inline const ::flatbuffers::TypeTable *CommandTypeTable() { // <- TypeTableがCommandTypeTable
static const ::flatbuffers::TypeCode type_codes[] = {
{ ::flatbuffers::ET_UCHAR, 0, 0 },
{ ::flatbuffers::ET_UCHAR, 0, 0 },
{ ::flatbuffers::ET_UCHAR, 0, 0 },
{ ::flatbuffers::ET_UCHAR, 0, 0 }
};
static const ::flatbuffers::TypeFunction type_refs[] = {
VideoProto::CommandTypeTable // <- 自分自身のポインタを指している
};
スキーマの型に対応するTypeCodeの値は以下です。
TypeCode | 対応するスキーマ型
------------------------------------
`ET_ULONG` | uint64
`ET_UINT` | uint32
`ET_UCHAR` | uint8 / enum
`ET_DOUBLE` | float64
`ET_STRING` | string
`ET_SEQUENCE | nested table
Windowsホスト → WSL2メモリ共有(動画配信)フロー
生成したC++ヘッダで定義された関数を使用しホストとクライアント間でメモリを共有する処理の流れは以下です。
╔═══════════════════════════════════════╗ ╔═══════════════════════════════════════╗
Windows 側(送信) WSL2 側(受信)
╠═══════════════════════════════════════╣ ╠═══════════════════════════════════════╣
CreateFrameMeta(fbb, ...) auto* frame =
↓ Offset<FrameMeta> GetVideoFrame(shared_mem_ptr);
fbb.CreateVector(pixels, size) ↓
↓ Offset<Vector<uint8_t>> frame->meta()->width()
CreateVideoFrame(fbb, meta, data) frame->meta()->height()
↓ Offset<VideoFrame> frame->pixel_data()->data()
FinishVideoFrameBuffer(fbb, root) ↓ const uint8_t*
↓ SDL_UpdateTexture(tex, ..., ptr)
memcpy --> 共有メモリ -------------------------> // ゼロコピー・デシリアライズ不要
║
╚═══════════════════════════════════════╝ ╚═══════════════════════════════════════╝
最後に
FlatBuffersでのデータ定義編集の文法と変換したシリアライザのコードの使い方を学ぶところで苦労しましたが、
- クロスプラットフォーム・多言語に対応
- 他のシリアライザ(JSONやProtocol Buffer)のようにパースしてオブジェクトに展開する手間がない
- バッファ上のデータを直接ポインタで参照可能(ゼロコピーアクセス)
に大きなメリットがあると感じました。
他にも前方・後方互換性や型安全、デシリアライズ不要のためヒープアロケーションが発生しないことも大きな特徴と思います。