C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

UE4/C++: JSON/XML/portable-binary 等の汎用シリアライズライブラリー cereal に UE4 の各種型を非侵入型アダプターで対応させるライブラリー cereal-UE4 を公開しました

cereal-UE4

GitHub - usagi/cereal-UE4: cereal ( C++ serialization library ) adapter for UE4 ( Unreal Engine ) types

cereal について

近年モダンな C++er にしばしば使われていると思われる JSON / XML / Binary ( platform native の endian に依存しない portable も可 ) に対応するシリアライザーのライブラリーです。ヘッダーオンリーライブラリーなので UE4 のプロジェクトへ取り込む際にも手間がほぼかかりません。

cereal-UE4 を作った経緯

  • UE4 標準の JSONリアライザーがしんどい。
    • FJsonSerializable 高レベル JSON シリアライズAPIhistoria - FJsonSerializebleマクロを使ってみる で紹介されているように使用可能な状況ではとても楽、便利だが…
      • 標準提供されている実装の範囲で対応している型が非常に限定的。例えば UE4 では頻出する FVectorFLinearColor などのシリアライズも対応できない。
      • FJsonSerializable を継承して型の対応を増やす事はできるが、わりと手間がかかる。
      • 継承の使用を前提とした侵入型シリアライザーなので UE4 の仕組み上 USTRUCT 型に適用できない(致命的にダメその1😅)。
      • 仮想関数を持つ FJsonSerializable の継承を前提とした設計のため、この方法でシリアライズ対応した型は仮想関数テーブルが必要となり非 POD 型(厳密には非標準レイアウト型)となるため、 TArray<FVector> のように連続でデータメンバー"だけ"がアライメントされ"密"に並んで欲しいデータ構造に適用できない(致命的にダメその2😅)
    • UE4/JSON/DOM 低レベルAPI はスマートポインターと参照が入り乱れた実装が必要となるため、手書きは複雑で疲れるし、コード量も大きくなり保守性も悪くなる。

と、言うわけで、 UE4JSONリアライザーは現状わりと残念なので、 UE4 の外の世界では C++er にわりと人気の高い(≃楽に使えてとても便利)な cereal を UE4 に対応する補助的なライブラリーを実装する事にしました。

cereal-UE4 の仕組み

cereal には大きく分けて侵入型と非侵入型の2つの方法で任意のユーザー定義型へのシリアライザー対応を追加できます。そこで、 cereal-UE4 では非侵入型で UE4 で使われる一般的な多くの型へ対応するシリアライザーをたくさん書いて、 cereal + cereal-UE4UE4 のプロジェクトへ追加すれば簡単に cereal による UE4 型に対応した JSON / XML / Binary のシリアライズが可能なよう実装しました。全ての実装は template 関数なので、実際に使用する型のシリアライザーのみフットプリントに影響します。

もちろん、 cereal-UE4 もヘッダーオンリーライブラリーです。 cereal と併せて簡単に UE4 プロジェクトへ導入できます。

使い方

UE4 のプロジェクトのディレクトリーの適当なところへ Thirdparty など適当なディレクトリーを用意し、

  1. cereal
  2. 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/includeThirdparty/cereal-UE4/includeUE4 プロジェクトの 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 下さい。