*新ステートセーブフレームワークについて。 Jul 03, 2018 K.Ohta 1.背景  武田俊也氏によるCommon Source Code Project (CSP)は、前に書いた文章でも書いてるように、 非常に設計の出来が良くて色々な人が参加しやすいように基本構造が出来ていて、しかも、GPLv2で 配布しているメリットが大きいわけですが、四五年ほど(もっとかも)ポーティングやFM-7シリーズを 移植するのにやっていると、もう少し改良したほうがよくないか?というものが度々出てきて、 ひんしゅくを買うかも知れないなどと震えながら、改良とか機能を「わがままに」追加してきました。 で、最近も色々作業をしていて、save_state()やload_state()で、Read/Writeのエレメント部分を ベタ書きしていて、その後に後処理をやってるので、バグを仕込む温床になるよねー。と言う感じの もにょりさを最近感じてました。 なので、   ・ステートのセーブロードは、エントリーを一個の関数で固めて行い、その関数が持ってる   エントリーリストに基づき、実際のデータをセーブもしくはロードできるようにする事で、   「セーブとロードで順番間違えたから、見た目正しくてもデータぶっ壊れてた」と言う悲   劇が出る余地を劇的に減らせる。  ・それでも「凡ミス」はつきまとうものですから、CCITT-CRC32演算を、各データブロックごと   に計算して、一個の塊の末尾に結果を置くと同時に、簡単なヘッダや識別子で、このデータは   なんのステートを表現してるか見えやすくする…要は、デバッグにも改変にも有効な構造に変える。   ・他のホストマシンでステートロード/セーブしたときでも、ステートデータが使えるように    しておきたい。となると、エンディアンなどの問題を解決しておかないといけない。 と言う辺りを実際にやろうと考えて作業しております。   2.実装系 2.1 クラス   csp_state_utils:: : セーブやロードを実行する為のクラス。             一つのデバイスやVM、その他の「もの」(DISK:: とか)ごとに、このクラスを             使ってセーブ・ロードする。(APIは次で) csp_state_data_saver:: 実際のデータを読んだり書いたりするクラス。通常はcsp_state_utils::             クラスの内部のみで使うが、呼び出す側で特殊な処理が必要な場合は、使用する。  又、実際の使用は、サポートルーチンやマクロを使って行うほうがよい。 2.1 扱うデータ型 (論理)数値 : bool , uintXX_t, intXX_t, float, double, long double 文字 (*1) : _TCHAR, _TCHAR * ピクセル : scrntype_t 固定長配列 : 1D_ARRAY, 2D_ARRAY, 3D_ARRAY 可変長配列 : VARARRAY_VAR クラス : FIFO*, CUR_TIME_T 特殊    : DECL_STATE_ENTRY_CMT_RECORDING() : テープ録音(VM)絡みの処理に使う。 (*1) 単純に_TCHARの配列 として宣言した場合には、配列、 DECL_STATE_ENTRY_STRING だと、0ターミネートの文字列として扱う 2.2 ファイル(今後分割するかも知れません) src/statesub.h : 使用者側が#include すべきヘッダ。必要な定義などは、このヘッダにまとめて読み込まれます。 src/state_data.h : 主に、csp_state_data_saver:: の内部定義が入ってる。 src/statesub.cpp : 処理実態の大半が入ってます。 src/common.cpp : calc_crc32() など、補助ルーチンを新設しました。 src/vm/device.h : エントリー定義に便利なメソッド定義をDEVICE:: にしてあります。 2.3 API(1) csp_state_utils::csp_state_utils(int _version = 1, int device_id = 1, const _TCHAR *classname = NULL, CSP_Logger *p_logger = NULL) : コアクラスのコンストラクタ。Qt版では、QObjectの継承をして構成されてる。  (Qtの SIGNAL/SLOTモデルのメッセージングに対応するため)。 tempate void csp_state_utils::add_entry(const _TCHAR *__name, T*p, int _len = 1, int _num = -1, bool is_const = false, int stride = 0); : セーブ項目変数を登録する。変数はポインタでなければならない。  次項で示す、DECL_STATE_ENTRY_foo系の宣言マクロを使うことを、 *強く*推奨する。 __name は、変数の名前、pは変数のあるポインタ(&var)、 _lenは、変数の*要素数* (length / sizeof(T))。 __numは、構造体の配列などで、メンバを記述する時に、配列内の 「番号」を記述するための変数、is_constはロード時に内容変更さ  れないフラグ。 strideは、構造体全体の大きさ。sizeof(struct foo)。 構造体の配列で、それぞれのメンバを定義するのに冗長さを抑え、 メモリ消費や速度も向上させる。 template void add_entry_vararray(const _TCHAR *__name, T **p, void *datalen, bool assume_byte = false, int __num = -1, int stride = 0); : 可変長配列(malloc/freeで管理される)を登録する。  配列の先頭のポインタのポインタをpに、変数の要素数(Tな変数の個数)を  格納する変数のポインタをdatalenに登録する。 void add_entry_fifo(const _TCHAR *__name, FIFO **p, int _len = 1, int __num = -1, int stride = 0); : FIFO型のエントリ関数 void add_entry_cur_time_t(const _TCHAR *__name, cur_time_t *p, int _len = 1, int __num = -1, int stride = 0); : cur_time_t 型のエントリ関数。ポインタ渡しでなければならない。 void add_entry_scrntype_t(const _TCHAR *__name, cur_time_t *p, int _len = 1, int __num = -1, int stride = 0); : scrntype_t 型のエントリ関数。ポインタ渡しでなければならない。 void add_entry_string(const _TCHAR *__name, _TCHAR *p, int _len = 1, int __num = -1, bool is_const = false); : 文字列型pを登録する。 void add_entry_cmt_recording(const _TCHAR *__name, FILEIO **__fio, bool* __flag, _TCHAR *__path); : ロードセーブ時に、テープに録音する動作をしていた時に、  リジューム動作が必要になるときのエントリ。 __fio : テープイメージセーブ用に使われるFILEIO::クラスの     ポインタ変数へのポインタ。 __flag: trueで録音が残ってると示すbool方変数へのポインタ。 __path: 読み書きしていた仮想テープのパス。 bool save_state(FILEIO *__fio, uint32 *pcrc = NULL); bool load_state(FILEIO *__fio, uint32 *pcrc = NULL); : add_entry_FOO()で登録したエントリをload/saveする。 __fio はsave_state(FILEIO *fio) のfio。 *pcrc は、CRC変数のポインタ(なくてもよい。) bool csp_state_data_saver::pre_proc_saving(uint32_t *sumseed, bool *__stat); bool csp_state_data_saver::pre_proc_loading(uint32_t *sumseed, bool *__stat); : ステートセーブ/ロード前の事前処理をする。  sumseed は、CRCを初期化したい変数のポインタ(通常、この変 数の初期値は0xffffffffである)。 __stat は、成功か失敗かを知るときの判定変数へのポインタ。 bool csp_state_data_saver::post_proc_saving(uint32_t *sumseed, bool *__stat); : ステートセーブが終了したときの処理。 sumseedにあるCRCを基に最終的な計算を行い、その値を追加する。 bool csp_state_data_saver::post_proc_loading(uint32_t *sumseed, bool *__stat); : ステートロードのときの後処理。  セーブ時に追加されたCRC値とsumseedから計算されたCRCを照合  して、あっていればtrueを返す。 2.4 API(2) エントリ宣言マクロなど FOO::decl_state() : そのデバイスなどのセーブステート項目を宣言する関数(スケルトンがDEVICE::にある) DEVICE::enter_decl_state(int version); : 各デバイスのdecl_state() 関数の冒頭に置くことで、  csp_state_utilsクラスをnew ・初期設定する。 DEVICE::leave_decl_state(); : del_state()の末尾につけると良い。 DECL_STATE_ENTRY_FOO(__name) : FOOは基本的な変数型やFIFO*など : 一個の要素を宣言する。 DECL_STATE_ENTRY_FOO_MEMBER(__name, __num) : FOOは基本的な変数型 : 一個の要素が__num目配列の要素のだと宣言する。 DECL_STATE_ENTRY_FOO_STRIDE(__name, __len, __stride) : FOOは基本的な変数型 : 大きさ__strideでああり、__len回の配列となってる構造体内の             特定要素__nameを宣言する。 DECL_STATE_ENTRY_1D_ARRAY(__name, __lenvar) : 1次元配列(要素数__lenvar)を定義する。__lenvar = sizeof(ARRAY) / sizeof(__name)。 DECL_STATE_ENTRY_2D_ARRAY(__name, x, y) : 2次元配列(要素数[x][y])を定義する。 DECL_STATE_ENTRY_3D_ARRAY(__name, x, y, z) : 3次元配列(要素数[x][y][z])を定義する。 DECL_STATE_ENTRY_STRING(__name, __len) : 文字列型変数__name (最大長__len - 1)を定義する。 DECL_STATE_ENTRY_VARARRAY_VAR(__name, __sizevar) : 可変長変数へのポインタ__nameが、__sizevarの値によってバイト数を  規定される事を宣言する。 uint8_t *__name = malloc(__sizevar); ... DECL_STATE_ENTRY_VARARRAY_VAR(__name, __sizevar); ... 注意:__sizevarのセーブ・ロードは、__nameより前に行うこと。 DECL_STATE_ENTRY_CMT_RECORDING(__fio, __flag, __path) :セーブでは:    __fioをつかって、仮想テープを「録音」してることを__flagが示した場合に、    書かれた仮想テープのデータをステートに書き込み、そのサイズも書き込む。    __flagが録音していないと示した場合、(uint32_t)0をステートに書き込む。 ロードでは: 録音データがセーブされていた場合は、__pathが示すファイルにそのデータを 書き込み直す。 3.セーブデータ構造  ・各データは、ビッグエンディアンのバイナリ値で記録される(例外あり)。  ・文字列型データは、\0ターミネートして記録される。  ・実数型(float, double, long double)は、アスキー文字列に変換された形で記録される。 0.0 = "0.0" + '\0' など。 ・各デバイスのステート(チャンクの)書式は以下のようになっている:   HEADER : ヘッダ ("CSP_SAVE" + '\0')   CLASSNAME : デバイス名(foo->this_device_name + '\0') DEVICE_ID : デバイスID(foo->this_device_id INT32) STATE_VER : デバイス内のステートのヴァージョン (INT32) INTERNAL_VER : ステートフレーム側のヴァージョン [バイナリデータ列] : DECL_STATE_ENTRY_fooで定義された実データの並び CRC32 : HEADER欄からバイナリデータ列末尾までのCRC32値をUINT32 Big Endianで記録する。 与えるSEED値は0xffffffff. ・VM::save_state()でセーブされるデータは、以下のような構造になっている: HEADER : "CSP_SAVE" + '\0' CLASSNAME : (例) "CSP::VM_HEAD" + '\0' DEVICE_ID : 通常0 STATE_VER : 各VMの(vm/foo/foo.cppの)STATE_VERSION INTERNAL_VER : 通常0 [バイナリデータ列] : [必要に応じて。0バイトの場合もある] CRC32 : [DEVICE #2 (通常Event)のステートチャンク)] [DEVICE #3 のステートチャンク)] ・ ・ ・ [DEVICE #n(最後のデバイス) のステートチャンク)] [EMU::側のフッター]     ※ DEVICE #1は通常DUMMY DEVICEであり、セーブ項目が、ない。 ---