本記事の目的
こちらの記事を元に、もう少しわかりやすく図式化や解釈を柔らかくして、スキーマの構造を理解していくために実施します。
CATS DSL(カタパルトのスキーマ用の独自言語)
CATS は、
・using
・enum
・struct
の3つの主要な宣言で構成されています。
スキーマは、スペースが重要です。
(これはPythonと同じようにインデントが非常に重要です。Javascriptなどでは空白を無視するであったり、インデントは見やすくするためという形で言語によってスペースを認識するのか?しないのか?そういった字句解析を実施するかといった話になります。
import
まずは重要なキーワードとしてimportを上げます。
用途としては別のファイルの内容を呼び出したいときに使用します。
importにもルールがあって現在このcatbufferに関するimportではファイルの指定の方法に規則性があります。
相対パス(ある基準を設定してからそこからどの地点にファイルがあるのか?)になっています。
このページを確認すると
python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol
との記載があるので、このschemas/symbolを相対パスの基準にしていると思われます。
参考として
この1行目には
import "transaction.cats"
と記載されている。
このtransaction.catsというのは以下のファイルになる
このファイル名を宣言して、構成しているので、おそらく相対パスの基準はschemas/symbolである可能性が高いと思います。
ここで宣言しているようにimportについてはコマンドライン引数としてパーサーに渡されたインクルードパスからの相対パスです。
python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol
--include ../schemas/symbol
このコマンドの--includeのところで指定できます。
サンプルコードも../schemas/symbolとなっているので、
基本的にはこの階層からの相対パスを検討した方がよさそうです!
using
Aliasステートメントを使用すると、
一意のPODタイプに名前を付けることができます。
CATSは、2種類のビルトインをサポートしています。
POD?:
Plain Old Data - Catbufferの文脈では、整数とバイト配列を意味します。
つまるところ型の表現を一意にしたい時、例えばPublicKeyとPrivateKeyは同じ型だが、意味合いとして別の表現をしたい時(読みやすさ向上のため)
定義できるのは整数とバイト配列です。
参考までに整数型(Rustのドキュメント)
https://doc.rust-jp.rs/book-ja/ch03-02-data-types.html#%E6%95%B4%E6%95%B0%E5%9E%8B
整数型はuintは符号なし、intは符号ありの表現
参考までにバイト配列(Rustのドキュメント)
バイト配列は文字列をスライスして一文字ずつ配列にしたもの
https://doc.rust-jp.rs/book-ja/ch04-03-slices.html#%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%B9%E5%9E%8B
ex:)
[0x68, 0x65, 0x6C, 0x6C, 0x6F]
バイト配列は16進数で表現されます。
また1バイトごとの配列になります。
整数型:符号なし("uint")と符号あり("int")、
サイズは{ 1, 2, 4, 8 }です。
例えば、8バイトの符号なし整数で表現される
Height型を定義する場合。
using Height = uint64
バイト対応表
符号なし | 符号あり | バイト数 |
---|---|---|
uint8 | int8 | 1byte |
uint16 | int16 | 2byte |
uint32 | int32 | 4byte |
uint64 | int64 | 8byte |
固定バッファタイプ:任意の長さの符号なしバイトバッファ。
例えば、32バイトの符号なしバッファで表される PublicKey型を定義する場合。
using PublicKey = binary_fixed(32)
16進数では1バイトを00 ~ FFという2文字で表して
それが32バイトの長さだから
16進数で表現すると64文字の長さになるということになります。
重要なのは、各エイリアスは一意の型として扱われ、
互換性を持って使用することはできないことです。
例えば、身長が想定されているところに体重を使用することはできませんし、
その逆も同様です。
型を定義した場合はその用途にしか使えません。
再利用性はなかなかなさそうですが、逆に混乱を招くのを防いでいます。
(おそらくスキーマ定義になるとそのような誤読の可能性を下げなければいけないのでしょう)
using Height = uint64
using Weight = uint64
高さの型定義は高さで使いましょう。
重さの型定義は重さで使いましょう。
using distance = uint64
using area = uint64
...
distance foo = distance(123); // OK
area bar = distance(123); // Compiler Error
distanceの型を定義した変数にdistanceの型を使うことは良くて
areaの型を定義した変数にdistanceの型を使うことはだめ
enum
enumは列挙型とも呼ばれて、何かを宣言した時にその変数がとりうるパラメータを事前に定義することができる。
例えば
TransportModeを定義して、そのとりうるパターンを設定したい時とかに使われます。
(自由度を低くすることで定義ミスを防ぐことが可能となるはずです)
たとえば、2 バイトの符号なし整数を使用した TransportMode列挙を定義する場合、次のようになります。
enum TransportMode : uint16
こと
列挙を構成する値は、インデントされた行で列挙宣言に続く。例えば、TransportMode列挙型にROAD(値0x0001)、SEA(0x0002)、SKY(0x0004)という3つの値を追加する場合、以下のようになる。
uint16なので16ビットの型、バイトに変換すると2バイトになるので、
16進数で表現すると4文字の長さになります。
enum TransportMode : uint16
ROAD = 0x0001
SEA = 0x0002
SKY = 0x0004
属性
ヒントは、属性を使って列挙に添付することができます。
列挙型は、以下の属性をサポートしています。
is_bitwise: 列挙体がフラグを表し、ビット演算をサポートする必要があることを示す。
たとえば、TransportMode 列挙体に is_bitwise 属性を設定する場合。
予想としてはこの属性のis_bitwiseを使用することで条件分岐のような処理をすることができるものと思われます。
bitwiseはビット演算のように2進数での演算を実施してくれます。
英語
https://en.wikipedia.org/wiki/Bitwise_operations_in_C
日本語
https://ja.wikipedia.org/wiki/%E3%83%93%E3%83%83%E3%83%88%E6%BC%94%E7%AE%97
struct(構造体定義)ちょっとここ難しいくいい解説ができていない
構造体は、複数のフィールドで構成されるユーザー定義の型を示すだけで、
データのみのクラスのようなものだと考えてください。
このstruct(ストラクト)でバイナリーデータのペイロード(symbolのREST APIので必須の情報)を定義するために使用されます。
なのでこのstructを読み解けばそれぞれのトランザクションのバイナリーデータの構造がわかるというわけです。
ここのメッセージボディを作成するためのルールを見ていきましょうといった認識です。
参照 Copyright (C) 2002-2022 ネットワークエンジニアとして All Rights Reserved.
まずは例から見ていきましょう。
abstract struct Vehicle
abstract: ジェネレータは,最終出力に型を含め,対応するファクトリを生成することが推奨される。
イメージとしては継承を使う想定で、その継承元としてabstractが使われるイメージがあります。
どちらかというとinlineとabstractで比較すると分かりやすいかと思います。
inline→モデル定義はするが単体で使われない
abstract→モデル定義をして単体でも使えるが他の構造体にもよく呼ばれる
構造体を構成するフィールドは、構造体宣言に続き、
インデントされた行で記述されます。
例えば、Vehicle 構造体に8バイトの値が
0から4,294,967,295までの範囲の符号なし整数型
のフィールドweightを追加する場合。
abstract struct Vehicle
weight = uint32
注意点
インデントは非常に重要です。
make_constは、
構造体レイアウトに現れない定数フィールドを定義するために使用されます。
例えば、2バイトの符号なし定数TRANSPORT_MODE(値はROAD)を定義する場合、
以下のようになります。
あくまでも構造体はフィールドと型を定義するので、
それ以外の要素に関してはmake_constで定義する
そのクラスには影響を与えないけど定数として持っておきたい情報とか
struct Car
TRANSPORT_MODE = make_const(TransportMode, ROAD)
make_reservedは予約フィールドを定義するために使用され、
レイアウトに現れ、デフォルト値を指定します。
例えば、1バイトの符号なし定数wheel_countを値4で定義する場合。
inline struct Car
wheel_count = make_reserved(uint8, 4)
sizeofは、他のフィールドのサイズで満たされるフィールドを定義するために使用される。
例えば、2バイトの符号なしcar_sizeを定義し、carフィールドの大きさで埋める。
inline struct SingleCarGarage
car_size = sizeof(uint16, car)
car = Car
このinlineは単体では意味をなさないけどinlineとして別の構造体に含まれる
ために用意している構造体ですよと識別できるようにしているっぽいです。
条件分岐
フィールドは、他のフィールドの値を条件とすることができる。
これは、いくつかの言語に存在する和集合の概念に近いものです。
CATSは以下の演算子をサポートしています。
equals: 参照フィールドの値が条件値と完全に一致する場合に、条件フィールドが含まれます。
not equals: 参照フィールドの値が条件値と一致しない場合に、条件フィールドが含まれる。
has: 参照フィールドの値にすべての条件フラグが設定されている場合、条件フィールドが含まれます。
not has: 参照フィールドの値がすべての条件フラグを持たない場合、条件フィールドが含まれる。
例えば、transport_mode が SEA のときだけ浮力があることを示すには、以下のようにする。
abstract struct Vehicle
transport_mode = TransportMode
buoyancy = uint32 if SEA equals transport_mode
特にequalsとnot equalsはいけそうな気がするけど
hasやnot hasは一部を含むみたいな表現が適切な気がする。
配列
動的なサイズの配列がサポートされています。
各配列は、定数、プロパティ参照、または特別な __FILL__
キーワードを
使用することができる関連するサイズを有しています。
例えば、Vehicles_count Vehicle 構造体で構成される
vehicles フィールドを持つ Garage を定義する場合、以下のようになります。
struct Garage
vehicles_count = uint32
vehicles = array(Vehicle, vehicles_count)
特別な __FILL__
キーワードは、配列が構造体の終わりまで拡張されることを示します。
__FILL__
を使用するためには、それを含む構造体が、
@size 属性によって指定されたバイト単位のサイズを含むフィールドを含んでいなければなりません。
例えば、Vehicle 構造体で構成される vehicles 配列が、
Garage 構造体のバイトサイズ garage_byte_size で終端まで拡張されることを示す。
@size(garage_byte_size)
struct Garage
garage_byte_size = uint32
vehicles = array(Vehicle, __FILL__)
__FILL__
ノーマルブロックでの参考としてこんな感じで使います。
参考
# variable sized transaction data
@alignment(8, not pad_last)
transactions = array(Transaction, __FILL__)
ここでは残りのメモリ領域を全てトランザクションの配列として処理します
みたいな意味になるそうです。(詳細はその時に)今は読み方なので。
⚠️配列の要素型(例で使用したVehicle)は、
固定サイズの構造体か、@size属性を付けた可変サイズの構造体でなければなりません。
inlines(構造体内部での他の構造体の展開)
スキーマの定義を再利用する方法です(DRY - Don't repeat yourself)
同じようなフィールドを多くの場所で重複して使用する必要があります。
構造体は、inlineキーワードを使って他の構造体の中にインライン化することができます。
多くの場所で類似のフィールドを重複して使用する代わりに、
スキーマ定義を再利用する方法です(DRY - Don't repeat yourself)。
例えば、2つのフィールドを持つCar構造体の先頭にVehicleをインライン化する場合。
struct Car
inline Vehicle
max_clearance = Height
has_left_steering_wheel = uint8
インラインは表示される場所に展開されるため、Carフィールドの順番は次のようになります。
: {weight, max_clearance, has_left_steering_wheel}. と同じように展開されます。
struct Car
weight = uint32
max_clearance = Height
has_left_steering_wheel = uint8
また、名前付きインラインは、参照されている構造体のフィールドに接頭辞を付けてインライン化します。
例えば、以下のSizePrefixedStringは、Vehicleではfriendly_nameとしてインライン化されます。
inline struct SizePrefixedString
size = uint32
__value__ = array(int8, size)
abstract struct Vehicle
weight = uint32
friendly_name = inline SizePrefixedString
year = uint16
以下は同じ内容になります
abstract struct Vehicle
weight = uint32
friendly_name_size = uint32
friendly_name = array(int8, friendly_name_size)
year = uint16
インライン構造内の __value__
は、
特殊なフィールド名であり、包含構造(Vehicle)で使用されている名前(friendly_name)に置き換えられる。
インライン化された構造体内の他の全てのフィールドの名前は、
包含構造体(Vehicle)で使用されている名前(friendly_name)の前にアンダースコアが付きます。
つまり、__value__
は friendly_name となり、size は friendly_name + _ + size または friendly_name_size となる。
属性
構造体には、属性を使ってヒントを付けることができます。
構造体には以下の属性がある。
is_aligned: すべての構造体フィールドがアライメントされた境界上に配置されていることを示す。
is_size_implicit: 構造体が sizeof(x) 文で参照される可能性があり、サイズの計算をサポートしなければならないことを示す。
size(x): x フィールドが(可変長の)構造体のフルサイズを含むことを示す。
initializes(x, Y): xフィールドがY定数で初期化されるべきであることを示す。
discriminator(x [, y ]+): (x, ...y)プロパティがファクトリー生成時の識別器として使用されるべきであることを示す(抽象構造体に対してのみ意味を持つ)。
例えば、transport_modeフィールドをTRANSPORT_MODE定数とリンクさせる。
ここら辺は実際のコードのタイミングで解説した方がいいのかと思います。
@initializes(transport_mode, TRANSPORT_MODE)
abstract struct Vehicle
transport_mode = TransportMode
struct Car
TRANSPORT_MODE = make_const(TransportMode, ROAD)
inline Vehicle
TRANSPORT_MODEは、任意の派生構造で定義できることに注意。
配列のフィールドは以下の属性をサポートする。
is_byte_constrained: サイズ値が要素数ではなく、バイト値として解釈されるべきであることを示す。
alignment(x, [[not] pad_last]): 要素が x-アラインの境界から始まるようにパディングされるべきであることを示す。
sort_key(x): 配列内の要素を x プロパティでソートすることを示します。
alignment が指定された場合、デフォルトでは、最後の要素は x-アラインの境界で終了するようにパディングされます。これは、pad_last修飾子を指定することで明示的に行うことができます。これを無効にするには、not pad_last 修飾子を指定することで、最後の要素をx-alignedの境界でパッドしないようにすることができる。
例えば、車両を重量でソートする場合。
struct Garage
@sort_key(weight)
vehicles = array(Vehicle, __FILL__)
コメント
#で始まるすべての行はコメントとして扱われます。コメント行が宣言や副宣言の直上にある場合、それは文書として扱われ、パーサーによって保存されます。そうでない場合は、破棄されます。
例えば、次の例では、「コメント1」は破棄され、「コメント2 コメント3」はHeightのドキュメントとして抽出されます。
# comment 1
# comment 2
# comment 3
using Height = uint64