UE4/C++: JSON/XML/portable-binary 等の汎用シリアライズライブラリー cereal に UE4 の各種型を非侵入型アダプターで対応させるライブラリー cereal-UE4 を公開しました
cereal-UE4
cereal について
近年モダンな C++er にしばしば使われていると思われる JSON / XML / Binary ( platform native の endian に依存しない portable も可 ) に対応するシリアライザーのライブラリーです。ヘッダーオンリーライブラリーなので UE4 のプロジェクトへ取り込む際にも手間がほぼかかりません。
cereal-UE4 を作った経緯
- UE4 標準の JSON シリアライザーがしんどい。
- FJsonSerializable 高レベル JSON シリアライズAPI は historia - FJsonSerializebleマクロを使ってみる で紹介されているように使用可能な状況ではとても楽、便利だが…
- 標準提供されている実装の範囲で対応している型が非常に限定的。例えば UE4 では頻出する
FVector
やFLinearColor
などのシリアライズも対応できない。 FJsonSerializable
を継承して型の対応を増やす事はできるが、わりと手間がかかる。- 継承の使用を前提とした侵入型シリアライザーなので UE4 の仕組み上
USTRUCT
型に適用できない(致命的にダメその1😅)。 - 仮想関数を持つ
FJsonSerializable
の継承を前提とした設計のため、この方法でシリアライズ対応した型は仮想関数テーブルが必要となり非 POD 型(厳密には非標準レイアウト型)となるため、TArray<FVector>
のように連続でデータメンバー"だけ"がアライメントされ"密"に並んで欲しいデータ構造に適用できない(致命的にダメその2😅)
- 標準提供されている実装の範囲で対応している型が非常に限定的。例えば UE4 では頻出する
- UE4/JSON/DOM 低レベルAPI はスマートポインターと参照が入り乱れた実装が必要となるため、手書きは複雑で疲れるし、コード量も大きくなり保守性も悪くなる。
- FJsonSerializable 高レベル JSON シリアライズAPI は historia - FJsonSerializebleマクロを使ってみる で紹介されているように使用可能な状況ではとても楽、便利だが…
と、言うわけで、 UE4 の JSON シリアライザーは現状わりと残念なので、 UE4 の外の世界では C++er にわりと人気の高い(≃楽に使えてとても便利)な cereal を UE4 に対応する補助的なライブラリーを実装する事にしました。
cereal-UE4 の仕組み
cereal には大きく分けて侵入型と非侵入型の2つの方法で任意のユーザー定義型へのシリアライザー対応を追加できます。そこで、 cereal-UE4 では非侵入型で UE4 で使われる一般的な多くの型へ対応するシリアライザーをたくさん書いて、 cereal
+ cereal-UE4
を UE4 のプロジェクトへ追加すれば簡単に cereal による UE4 型に対応した JSON / XML / Binary のシリアライズが可能なよう実装しました。全ての実装は template
関数なので、実際に使用する型のシリアライザーのみフットプリントに影響します。
もちろん、 cereal-UE4 もヘッダーオンリーライブラリーです。 cereal と併せて簡単に UE4 プロジェクトへ導入できます。
使い方
UE4 のプロジェクトのディレクトリーの適当なところへ Thirdparty
など適当なディレクトリーを用意し、
cereal
cereal-UE4
を GitHub から clone なり zip なりで調達、配置します。
<MyProject> |-Thirdparty | |-cereal | |-cereal-UE4 |- MyProject.uproject |- ...
それから、 MyProject.Build.cs に、
using System.IO;
を冒頭に追加しつつ、
public class MyProject
内の public MyProject( ReadOnlyTargetRules Target )
コンストラクターの中に、
{ var base_path = Path.GetDirectoryName( RulesCompiler.GetFileNameFromType( GetType() ) ); string third_party_path = Path.Combine( base_path, "..", "..", "Thirdparty"); PublicIncludePaths.Add( Path.Combine( third_party_path, "cereal", "include") ); PublicIncludePaths.Add( Path.Combine( third_party_path, "cereal-UE4", "include") ); }
こんな具合で Thirdparty/cereal/include
と Thirdparty/cereal-UE4/include
が UE4 プロジェクトの C++ コードファイルのコンパイルの際にインクルードパスに追加される設定を追加します。
あとは、ただの struct
でも class
でも USTRUCT
でも UCLASS
でも気にせず、シリアライズしたいクラス、例えば以下のような FMySomething
型に対して、
// これはあなたのお好きなように用意する USTRUCT( BlueprintType ) struct FMySomething { GENERATED_BODY() UPROPERTY( BlueprintReadWrite ) FString String; UPROPERTY( BlueprintReadWrite ) FDateTime DateTime; UPROPERTY( BlueprintReadWrite ) FVector Vector; UPROPERTY( BlueprintReadWrite ) TArray< int32 > ArrayI32; };
// お好きなように cereal の非侵入型シリアライザーを追加する。 // 型の定義の直後に .h へ書いてもいいし、シリアライズを特定の .cpp でしか行わないのならその .cpp へ書いてもいい // cereal #include "cereal/cereal.hpp" #include "cereal/archives/json.hpp" // cereal-UE4 #include "cereal-UE4.hxx" // std::stringstream #include <sstream> template < typename A > void serialize( A& a, FMySomething& in ) { a ( cereal::make_nvp( "String", in.String ) , cereal::make_nvp( "DateTime", in.DateTime ) , cereal::make_nvp( "Vector", in.Vector ) , cereal::make_nvp( "ArrayI32", in.ArrayI32 ) ); }
// これはどこか、 BeginPlay と EndPlay とか、お好きなタイミングでシリアライズして構わない。 // もちろん、 FMySomething はローカル変数でなくてもよいし、クラスのメンバーでもなんでもよい。 FMySomething MySomething; MySomething.String = "Hello, こんにちは."; MySomething.DateTime = FDateTime::UtcNow(); MySomething.Vector = FVector::UpVector(); MySomething.ArrayI32 = { 123, -456, 789 }; // JSON へシリアライズ std::stringstream buffer; { cereal::JSONOutputArchive a( buffer ); a( cereal::make_nvp( "MySomething", MySomething ) ); } const auto json = FString( buffer.str().data() ); UE_LOG( LogTemp, Log, TEXT( "%s" ), *json ) // おまけ: FString をファイルへ書き出したければこんな具合 if ( ! FFileHelper::SaveStringToFile ( json , *( FPaths::ProjectDir / TEXT( "MySomething.json" ) ) , FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM ) ) { UE_LOG( LogTemp, Fatal, TEXT( "Save Error" ) ); }
これで次のような MySomething.json
が書き出される
{ "MySomething": { "String": "Hello, こんにちは.", "DateTime": "2018-03-29T17:59:50.595Z", "Vector": { "X": 0.0, "Y": 0.0, "Z": 1.0 }, "ArrayI32": [ 123, -456, 789 ] } }
読み出し(デシリアライズ)したい場合は
FMySomething from_json; std::stringstream buffer; buffer << TCHAR_TO_UTF8( *json ); cereal::JSONInputArchive a( buffer ); a( from_json );
これで from_json
には to_json
と同じ中身が復元される。
おわりに
cereal-UE4 は MIT ライセンスの OSS として GitHub へ公開しました。 UE4/C++er が JSON / XML / Binary のシリアライザーについて、標準実装では不足、あるいは面倒そうな状況に対し、 cereal-UE4 が助けとなれば幸いです。対応する型や、すべての型での実装例はリポジトリーの README.md や example/ を参照して下さい。対応型の不足やバグ修正があれば PR 下さい。