Theolizer
Version.1.2.0
serializer for C++ / Do you want to update your classes easily ?
|
まず、「シリアライズ」という用語について
シリアライズ(Serialize)の逆はデシリアライズ(Deserialize)なのですが、この資料ではデシリアライズも含めて「シリアライズ」と呼んでいます。
シリアライザも同様にデシリアライザを含めて呼んでいます。
両方を含めた用語が欲しいため、このようにしました。ご了承下さい。
区別したい時は、シリアライズを保存、デシリアライズを回復と呼んでいます。
① プリミティブ型
基本的な型をプリミティブ型と呼んでいます。下記がプリミティブ型です。
② enum型
通常のenum型、および、scoped enum型の両方に対応しています。
③ class、struct
class、および、structは同じ扱いで、両方に対応しています。
④ C++静的配列
①~④のC++静的配列に対応しています。(多次元配列も対応しています。)
⑤ ポインタ型
①~④へのポインタです。 デシリライズ時、アドレスのみ回復します。
⑥ オーナー・ポインタ型
①~④へのポインタで、ポイント先メモリ領域の管理(獲得/開放)のために使われているポインタです。
シリアライズ時にポインタが指していた先のメモリ領域も、デシリアライズ時に回復します。
⑤とは異なることをTheolizerへ通知するため、ソース上で指定します。これをオーナー指定と呼びます。下記マクロで指定します。
● シリアライズできないもの
下記はシリアライズに対応していません。
char*
)はデシリアライズ処理のメモリ獲得方法を決定できないため、サポートしていません。char*
型は1文字へのポインタとして処理します。クラスは下記の3種類の形式に対応しています。
デシリアライズ時のメンバ対応方法
非侵入型完全自動と侵入型半自動は、自動的にメンバ変数を枚挙して、シリアライズします。
そして、デシリアライズする際にシリアライズされたデータとメンバ変数の対応を取る方法として2つ用意しました。
クラス・テンプレートのシリアライズについて
Theolizerの内部的には(標準コンテナに対応するため)クラス・テンプレートにも対応しています。
しかし、使い方が難しいだけでなく、自動テストの組み合わせも膨大になりそうです。そのため、当面はクラス・テンプレートは非公開と致します。
将来的に自動テストを記述した後、公開したいと考えています。
enum型をシリアライズする際にどんな形式で保存するのか選択できます。
例えば下記のようなenum型を想定します。
どちらの場合も一致する文字列や値が無い場合は、警告を出力します。
また、enum型は下記の2種類の形式に対応しています。
「参照」は、参照先インスタンスの別名として取り扱います。
通常のインスタンスと同様に保存/回復可能です。
また、ポリモーフィズムに対応しています。
派生クラスのインスタンスを基底クラスで参照されていた場合、派生クラスを保存/回復します。ただし、保存時と異なる派生クラスを参照している場合はエラーとします。参照はその参照先を初期設定後、変更出来ないというC++仕様によるものです。
「2-4.同じ領域を複数回シリアライズする時の動作について特記事項 」参照。
クラスのメンバ変数に保存先を指定し、1つのクラスのメンバ変数を異なるファイルに保存したり、別途通信回線で送信したりできます。
事前にメンバ変数1つ毎に保存先を複数指定しておきます。
そして、シリアライザ・オブジェクトのコンストラクト時も保存先を指定します。
シリアライザに指定された保存先がメンバ変数に指定された保存先と一致した場合、そのメンバ変数は保存されます。なお、回復(デシリアライズ)は以下の通りです。
上記名前対応時の仕組みにより、あるメンバ変数の保存先を異なるファイルへ変更した時でも(適切な順序でファイルから回復すれば)、当該メンバ変数を回復できます。
現在提供しているシリアライザは以下の通りです。
theolizer::u8string
を提供しています。標準コンテナ(STL)は枚挙できないものを除き対応予定です。現在は下記に対応しています。
現在、下記のスマート・ポインタに対応しています。
コンパイルしているファイル名に".theolizer.hpp"を加えた名前のファイルへ自動生成します。
これをシリアライズ対象クラス定義とシリライズ処理(シリアライザのコンストラクト)の間で::includeして下さい。
また、下記マクロにシリアライザのインスタンスを渡すため、これらのマクロを使用する前にシリアライザをコンストラクトしておく必要が有ります。
ソース・ファイルの先頭に下記を記述することでドライバが解析処理をスキップしますので、コンパイル時間を短縮できます。
#define THEOLIZER_NO_ANALYZE
デフォルトでは、エラーを検出したら、theolizer::ErrorInfo例外を投ます。シリアライザのコンストラクト時に、例外を投げないよう指定した場合、theolizer::ErrorInfo例外を投げません。
ただし、全ての例外を投げないわけではありません。下記については必要であればユーザ側にて投げないよう設定下さい。
最後に発生したエラーは、theolizer::ErrorReporter::getError()で受け取ります。
また、シリアライザはエラーが発生すると以降の処理要求は全て何もしません。頻繁にエラー・チェックしなくても実害がでないようにするためです。
更に、例外禁止の時、エラー処理漏れを防ぐため、エラー状態のままシリアライザ・オブジェクトをデストラクトするとエラーを受け取っていないものとしてabort()します。
abort()しないためにシリアライザのインスタンス.resetError();
でエラー状態をリセットして下さい。
シリアライザのインスタンスはスレッド安全性を保証しません。
同じシリアライザのインスタンスに対してTHEOLIZER_PROCESS等のマクロを呼び出す際の順序は、保存時と回復時で一致させておく必要が有るため、マルチ・スレッドから呼び出ししはいけないためです。
異なるシリアライザのインスタンス間ではスレッド安全性を保証しています。
例えばint型のサイズが異なる処理系間でint型データを確実に交換できるようにする機能はありません。
これは原理的に不可能です。
マルチプラットフォーム対応が必要な場合は、データ交換するプリミティブ型変数にはint32_t等のサイズが確定している型を用いて下さい。
原則として、シリアライズするクラスにはデフォルト・コンストラクタは不要ですが、一部必要なものがあります。
それはオーナー指定ポインタとしてシリアライズする非侵入型完全自動クラスと侵入型半自動クラスです。
オーナー指定ポインタが以下のどちらかの場合に、デシリアライズする際に対象クラスをデフォルト・コンストラクタで生成するためです。・・・①
なお、①の条件の時、非侵入型手動についてはユーザ定義のloadClassManual()関数内でコンストラクトするため、ユーザ側にて呼び出すコンストラクタを決定できます。
これは自動シリアライズの対象としていません。
現在、仮想基底クラスは非対応です。
オブジェクト追跡することで対応できる可能性はありますので、有用性が高い場合に対応を検討します。
現在のTheolizerでは2点制約があります。
ポインタは全てオブジェクト追跡します。そして、全てのポインタでない変数についてもオブジェクト追跡すれば確実にアドレス解決できるのですが、ポイントされていないものまで追跡するのは無駄が多いです。
そこで、ポインタでない通常の変数は下記のように追跡指定するようにしました。
また、配列については、その1つ1つの要素をオブジェクト追跡します。配列全体は追跡しません。
例えば、int foo[3];の時、foo[0], foo[1], foo[2]を追跡しますが、foo全体は追跡しません。
foo[0]のアドレスとfoo全体の先頭アドレスは同じアドレスですが型が異なるため、異なるオブジェクトとしてとして取り扱うためです。
オブジェクト追跡はインタンス実体のアドレスを追跡しますので、ポインタの指すインスタンスが同じオブジェクト追跡単位内に記録されている必要があります。
そのオブジェクト追跡単位は、下記の期間です。
基底クラスのオーナー・ポインタ型に対して、派生クラス・インスタンスの回復が可能です。
ポリモーフィズム対象の派生クラスをTHEOLIZER_REGISTER_CLASS()マクロで指定して下さい。
なお、現在のところクラス・テンプレートはポリモーフィズム非対応です。
同じインスタンスを複数回シリアライズするケースがあると思います。
例えば、参照の指し示す先を動的に切り替え、かつ、実体側と参照側の両方をシリアライズする場合に発生する可能性があります。
Theolizerは各インスタンスに対して、被ポインタ指定することでオブジェクト追跡しますが、オブジェクト追跡している場合、同じインスタンスであることを自動判定できます。
そこで、同じオブジェクト追跡単位内で重複してシリアライズした場合、先頭の1つのみをシリアライズします。
また、同じインスタンスには同じオブジェクトIDを振って保存しています。回復時、同じオブジェクトIDが振られたインスタンスは同じメモリへ回復される必要があります。それができない時はWrongUsing例外を投げます。このエラーはポインタの回復時は発生しませんが、参照の場合、参照先を変更することができないため発生する可能性かあります。
動作をまとめると以下のようになります。
状況 | 動作 |
---|---|
オブジェクト追跡しているインスタンスを 複数回シリアライズした時 | 2回目以降はシリアライズされない。 回復処理時、回復先が異なるとWrongUsing例外を投げる。 |
オブジェクト追跡していないインスタンスを 複数回シリアライズした時 | 全てシリアライズされる。 |
なお、アップデートに関しては、新しいプログラムが保存したデータを古いプログラムが回復することをサポートしません。
クラスとenum型について、バージョン番号を1から1つづ上げていくことができます。
クラスは、1つ上げる毎にバージョン・ダウン処理(downVersion)、バージョン・アップ処理(upVersion)を記述できます。
enum型は、古いシンボル名やシンボル値を別のシンボルへ割り当てたい時にバージョンを上げることで対応できます。
クラスとenum型自身のバージョン番号を「ローカル・バージョン番号」と呼びます。
Theolizerは旧プログラムのバージョン番号を指定してデータ保存する機能に対応していますが、その際に全てのクラスとenum型について適切なバージョン番号を指定することはたいへん困難です。
関連する複数のクラスについて同時にバージョン・アップすることで定義が矛盾しないようにすることも良くあると思います。そのようなクラス群について、全て矛盾なくローカル・バージョンを指定する必要があるからです。
プログラマが1つ1つ指定することは現実的ではないため、Theolizerはグローバル・バージョン番号テーブルを自動生成します。
グローバル・バージョン番号は、1つ以上のクラスかenum型のローカル・バージョン番号を上げた時にインクリメントして下さい。回復対象のシリアライズ・データ内の全ローカル・バージョン番号を特定できるように上げればOKです。
Theolizerドライバは、グローバル・バージョン番号に対応する(完全自動型を除く)全てのローカル・バージョン番号を「グローバル・バージョン番号テーブル」へ自動的に生成します。(完全自動型のローカル・バージョン番号は1固定ですので記録する必要がありません。)
旧プログラム形式でデータ保存する時は、このグローバル・バージョン番号を指定することで、全てのローカル・バージョン番号を矛盾なく指定できます。
また、シリアライズ・データにもグローバル・バージョン番号を記録します。これにより、当該シリアライズ・データを回復する際の初期ローカル・バージョン番号も全て矛盾なく特定できます。
この番号を用いて、各クラス、および、enum型を回復後、カスケードにバージョン・アップ処理を行うことで最新版のデータを回復します。
クラスAに含まれるクラスBのメンバ変数x、および、基底クラスyについて、down/upVersion関数でx, yのメンバにアクセスしたいケースがあります。
そして、クラスAをバージョン・アップする際にクラスBもバージョン・アップされることもあります。
このような場合で、クラスAのdown/upVersion関数を記述する際に、クラスBの当時のバージョンのメンバ変数を使いたいこともあると思います。
以下の条件を満たした基底クラスとメンバ変数については、それを含むクラスと「足並みを揃えて(Keep-step)」バージョン・ダウン/アップ処理され、各down/upVersion関数を定義した当時のメンバを提供します。
逆にKeep-step処理しないものは以下の通りです。
なお、配列については配列の基底型により上記の通りKeep-step処理を判定します。
追加されたメンバがある場合、もしくは、保存先指定でクラス分割されている場合、旧バージョン・データから回復した時、ファイルに記録されていないメンバについては回復されません。従って、upVersion処理にて回復されていないメンバが存在する可能性があります。
そこで、down/upVersion関数を記述する場合は、追加した変数をdown/upVersion関数内で使用しないこと、できるだけクラス分割しないことお勧めします。
クラス分割とdown/upVersion処理の両方が必要な場合は下記をお薦めします。
downVersion後upVersionで元に戻らないような修正を行うことは多いと思います。(例えは、downVersion関数を記述せずupVersion関数のみを記述すると該当します。)
何も手当しない場合、シリアライズ・データを回復する時、回復対象でないメンバがdown/upVersion関数処理の結果、不適切に変化してしまいます。
これを避けるため、以下の対策を実装しています。
少しややこしいのでまとめます。
2-1.はシリアライズ対象のインスタンスへ直接アクセスすることになります。
そのため、これらをdown/upVersion関数内で修正した場合、その修正は直接ターゲットが書き換えられてしまいます。そのため、保存時、および、回復処理で回復されなかった変数が変化してしまいます。クラスの「外」にあるインスタンスは書き換えないようにご注意下さい。
なお、2-2.と2-3.については代入演算子(operator=)をprivate定義していますので、変更しようとするとコンパイル・エラーになります。
バージョン・アップに伴い、一度シリアライズ指定したクラスやenum型をソース・コードから削除したい場合の注意事項があります。
そのような削除したいクラスやenum型を仮に 型X と呼びます。
最新版の別のクラスの旧バージョンで 型X を使っている場合
最新版で一切使っていなくても、旧バージョン・データから回復する際に 型X の定義が必要になりますので、最新版のソースから 型X の定義を削除しないで下さい。
型X を一切使っていない場合
この場合は原則として削除可能です。ただし、グローバル・バージョン番号テーブルに 型X が登録されています。その自動削除には対応していませんので、手で削除する必要があります。
THEOLIZER_GLOBAL_VERSION_TABLEマクロを定義(2-2.グローバル・バージョン番号テーブル実体定義 )したコンパイル単位の *.theolizer.hpp の最後にありますので、その中の下記行を削除して下さい。
これを削除し忘れていた場合、コンパイル・エラー(Global Version No. Table error. Please check deleted class or enum.)となります。