SDKの仕組み
まずは新たなSDKを作成するにあたり簡単に仕組みを紹介します
シリアライズとデシリアライズ
Symbolのトランザクションはシリアライズしてbinaryにし、署名後ノードにputし(binaryはhexで文字列とする)承認を待ちます。
そのため、SDKの最重要項目はトランザクションをシリアライズすること、またbinaryを元にデシリアライズしトランザクションを構築すること(だと思ってます)です。
またSDKに最低限必要なのは署名やコンバーター、IDジェネレータなどのユーティリティツール、各種ネットワークに対応(symbolとnemや他別プライベートチェーン等)を容易にするためのfacade等が考えられます。
javascriptやpythonではトランザクション構築を容易にするためのparserが存在しますが、これは無くても問題ないので今回は省きます。(C#では頑張って作りましたが使ってない、もちろん追加も可能なので今後需要があれば)
Models
SDKのコアとなるトランザクションに関する全てのクラス(シリアライズやデシリアライズ関数を持つ)はparserを使ってschemasを読み込み生成します。
catparser
はCATSと呼ばれるDSLをyaml形式で出力するために存在します
CATS DSL
について公式のドキュメントは以下があります
DeepLで和訳したものは以下
SymbolのCATSはcatbufferと呼びます。
generator
先程yaml形式で出力と書きましたがこれはデフォルトでの場合です。parserの引数generatorにその言語用のgeneratorを指定することでyamlではなくgenratorに沿った形式で出力できます
つまり、新たな言語用SDKを作成する場合、このgeneratorを作成することにより基盤となるクラス(や構造体等)群(以後modelsと呼ぶ)を出力することができます。
その他のクラス等
generatorによって生成されたmodelsの内部で使用するユーティリティ関数や、生成されたトランザクションに署名するためのクラスなど他にもいくつか必要なクラスがあるのでこれは別途自作します。
種類
Modelは大きく分けて5種類でIntタイプ、Arrayタイプ、Enumタイプ、Structタイプ, Factoryタイプです。Factory以外のタイプはdeserialize()
, serialize()
, size
を持ちます。sizeは固定のものと変動するものがあります。
Intタイプ
数値型です。BaseValueを継承しvalueを持ちます。sizeは1, 2, 4, 8のどれかで、これは1byte,2byte,4byte,8byteを意味しvalueの最大値です。
この先SDKを作成する上で重要なbitとbyteの話。本記事では事前知識があったほうが良いけど知ってる人はスルーで良い場合、折りたたんでおきます。知ってると知らないでは大違いなので知らない場合は理解してから進んでください。
bit, byte, signed, unsignedの話
Intタイプのsizeは4種類ですが、これはbyte数を表します。1byteは8bitです。1bitは0か1のどちらかを表します。
1byte 8bitの最小値と最大値は
2進数 | 10進数 |
---|---|
00000000 | 0 |
11111111 | 255 |
です。下位から順に1,2,4,8,16,32,64,128で全部足すと255ですね
2byteだと16bitなので
1111111111111111 が最大で8bitが2個並んでます。つまり最大値は256 * 256 = 65536です。が、0から始まるので65535が最大です。
同じく4byteは4,294,967,295で、8byteは18,446,744,073,709,551,615です。これらの数値以内であれば取り扱えるということです。
ただし、これはunsigned(符号なし)の話で、signed(符号あり)の場合は変わります。また今後のためにもbyte単位ではなくbit単位で覚えたほうがいいです。なぜかというとだいたいの言語ではbitで関数名が決められていることが多いからです。かつ、SDK内でよく使う単語だからです。
signed | 範囲下限 | 範囲上限 |
---|---|---|
8bit | -127 | 128 |
16bit | -32768 | 32767 |
32bit | −2,147,483,648 | 2,147,483,647 |
64bit | −9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
どちらも実際に取り扱える数字の範囲は同じです。
ちなみにUint8Arrayとはつまり8bitのunsigned int(符号なし整数)です。
作成したい言語が符号なしで64bitを表せるならそっちのほうが分かりやすいのでそのほうがいいと思います。
Arrayタイプ
Byte配列型です。ByteArrayを継承します。Byte配列であるbytesを持ちます。sizeは固定でこのbytesの長さです。
例えばPublicKeyのsizeは32です。つまり32byteです。
Enumタイプ
Enum型です。が、実際にEnumかどうかは言語によります。EnumライクでOK。valueを持ちsizeはvalueの範囲です。
Enumタイプは2種類あります。ひとつは、NetworkTypeのように状態を判別するためのもの。もう一つはフラグで、valueを元にフラグを判別します。例えばMosaicFlagsです。
flagの話
例えばmosaicFlagsはEnumですが NONE(0), SUPPLY_MUTABLE(1), TRANSFERABLE(2), RESTRICTABLE(4), REVOKABLE(8)の5つのフィールドを持ちます。ただし、NetworkTypeのようにどれか一つではなく複数選択することも可能です。 これをbitで判別します。もし全てのフラグが立っていれば1 + 2 + 4 + 8 = 15 になり、MosaicFlagsのvalueは15です。
これを2進数で表すと00001111で、REVOKABLEがtrueかどうかを判別するには
00001000のAND演算でしたから4つ目のbitが1ならtrue、0ならfalseになります。今回のように15であれば
00001111 & 00001000 = 00001000なのでREVOKABLEはtrueです。このようにflagタイプはvalueを元にどのフラグが立っているかを判別することができます。
Structタイプ
TransactionはもちろんMosaicもStructタイプで、複数のプロパティを持つものはStructタイプになります。
sizeはそれらプロパティサイズの合計です。シリアライズやデシリアライズもプロパティをそれぞれシリアライズやデシリアライズしたものの連結になります。
Factoryタイプ
TransactionやBlockなど、ペイロードを元にデシリアライズするためのタイプです。sizeやserializeはありませんが、createByNameという関数を持ち文字列を引数にTypeを返します。
準備
今回はサンプルとしてDartのSDKを作成してみたいと思います、なお筆者はDartの知識は全くありません。どうやら人気の言語なので需要ありそう、ということで取り上げてみました。そのため、ツッコミどころ満載になる可能性があります。最後にリモートリポジトリのリンクを貼りますので詳しい方はぜひもっと良いものにしていただければありがたいです。MITライセンスなので好きにしていただいて構いません。
整数型について
言語によって扱える整数型が変わってきます。64bit整数が扱えれば問題はありませんが、javascriptのようにBigIntを使わなければいけない場合もあります。またdartの場合はsigned64bit整数はつかえますがunsigned64bit整数は使えませんでした。flutterでwebで使いたい場合にこれはエラーになります。そのためBigIntを使うことにしました。
clone
git clone https://github.com/symbol/symbol.git
cd symbol/sdk
コピー
1から作成するより既存のものをコピーして編集するほうが早いので、今回はpythonのSDKをベースに編集します。
cp -r python dart
dartプロジェクト作成
今回はdartのsdkを作成するのでプロジェクトを作成しておきます。言語に合わせてください。
cd dart
dart create symbol_sdk
削除
不要なファイルの削除
だけど、とりあえず今はいいや
scripts
parserを起動するためのシェルスクリプトを編集します。出力フォルダの指定、ファイルの拡張子の変更程度です。
#!/bin/bash
set -ex
function generate_code() {
# $1 blockchain
# $2 destination
local git_root
git_root="$(git rev-parse --show-toplevel)"
PYTHONPATH="${git_root}/catbuffer/parser" python3 -m catparser \
--schema "${git_root}/catbuffer/schemas/$1/all_generated.cats" \
--include "${git_root}/catbuffer/schemas/$1" \
# 出力先を書き換える、先程dartプロジェクトを作成したのでそこのlibフォルダにした
--output "${git_root}/sdk/dart/symbol_sdk/lib/$2" \
--quiet \
--generator generator.Generator
}
if [[ $# -eq 0 ]]; then
echo "updating generated code in git"
for name in "nem" "symbol";
do
# 拡張子を書き換え
rm -rf "./src/${name}/models.dart"
generate_code "${name}" "${name}"
done
elif [[ "$1" = "dryrun" ]]; then
echo "running dryrun diff"
for name in "nem" "symbol";
do
generate_code "${name}" "${name}2"
diff --strip-trailing-cr "./src/${name}/models.dart" "./src/${name}2/models.dart"
rm -rf "./src/${name}2/models.dart"
done
else
echo "unknown options"
exit 1
fi
Generator
generatorのファイルは以下です。
- Generator.py
- AbstractTypeFormatter.py
- TypeFormatter.py
- PodTypeFormatter.py
- EnumTypeFormatter.py
- StructTypeFormatter.py
- FactoryFormatter.py
- printers.py
- format.py
- name_formatting.py
こちらを読んでおいてください
*DeepL和訳
それぞれ簡単に説明しておきます。
AbstractTypeFormatter
抽象クラスのフォーマッターです。他のフォーマッターの基底クラスとなります。あまり触ることはありませんが、他のフォーマッター内で共通で使用したい物などは追加します。例えばdartではinterfaceと使いたかったのでget_interface
を追加しました。
TypeFormatter
このファイルにはClassFormatterとTypeFormatterがあります。
各クラスで共通で使用されるコンストラクタや関数のガワとなる箇所を決めるフォーマッターです。
ClassFormatterではdartを例にするとクラスなら
class Amount extends BaseValue implements IDeserializable {
// body
}
この部分
あと、各関数は全てgenerate_methodで受け取るのでそれをメソッド名や引数、戻り値などをどうデザインするかをここで決めます。
PodTypeFormatter
IntタイプとArrayタイプのクラス用のフォーマッターです。
EnumTypeFormatter
Enumタイプ用のフォーマッターです
StructTypeFormatter
Structタイプ用のフォーマッターです
FactoryFormatter
Factoryタイプ用のフォーマッターです
printers
各タイプのシリアライズやデシリアライズ時に使用します。例えばStructタイプのデシリアライズには各フィールドのデシリアライズをしますが、そのフィールドがIntタイプであればIntPrinterを使います。
他にもsortやcompareなどの関数の中身もあります。
load()はデシリアライズ時の変数の戻り値、store()はシリアライズ時のフィールドの戻り値と覚えておくと良いかと。
format
indentなどフォーマットする際の関数置き場。
name_formatting
各フィールド名はcatbufferから読み込みますがこれはtransfer_transaction_body_reserved_1
のようにスネークケースです。言語によってはキャメルケースやパスカルケースなど様々だと思います。その命名規則用の関数がここあります。
ファイルは以上です。追加する必要はないと思うので、これらを全て編集していきます。
自分の場合は最初はなかなか理解できなかったのでまず、自作でmodelを記述し、出力しながら意図したものが仕上がるか都度確認していました。
今となると理解が深まっているのですが、全部理解してからgeneratorを書き始めるか、都度確認しながら理解を深めるか、これはどちらがいいか分かりません。前者が容易であればそのほうが良いとは思いますが、自分の場合は徐々に理解しなければ頭がついてこなかったので後者で進めました。
今回は自分の理解がついてきているので折衷案として事前に変更できるものは行い、出力しながらのほうが良いものは都度確認しながら作成するような方法を取りたいと思います。
Generator.py
#!/usr/bin/python
from pathlib import Path
from catparser.DisplayType import DisplayType
from catparser.generators.util import build_factory_map, extend_models
from .EnumTypeFormatter import EnumTypeFormatter
from .FactoryFormatter import FactoryClassFormatter, FactoryFormatter
from .PodTypeFormatter import PodTypeFormatter
from .printers import BuiltinPrinter, create_pod_printer
from .StructTypeFormatter import StructFormatter
from .TypeFormatter import TypeFormatter
def create_printer(descriptor, name, is_pod):
return (create_pod_printer if is_pod else BuiltinPrinter)(descriptor, name)
def to_type_formatter_instance(ast_model):
type_formatter_class = {
DisplayType.STRUCT: StructFormatter,
DisplayType.ENUM: EnumTypeFormatter,
DisplayType.BYTE_ARRAY: PodTypeFormatter,
DisplayType.INTEGER: PodTypeFormatter
}[ast_model.display_type]
return type_formatter_class(ast_model)
def generate_files(ast_models, output_directory: Path):
factory_map = build_factory_map(ast_models)
extend_models(ast_models, create_printer)
output_directory.mkdir(exist_ok=True)
# ここの出力ファイル名の拡張子を書き換える 例)models.dart
with open(output_directory / 'models.dart', 'w', encoding='utf8', newline='\n') as output_file:
output_file.write(
# ここはmodelsファイルのヘッダ箇所,import等を行うが現時点では何が必要か全くわからない(このQiitaに書きながらdartについて調べながら進めてます)ので空白,言語によっては名前空間の指定やライブラリのインポートなどをしておく
'''
'''
)
for ast_model in ast_models:
generator = TypeFormatter(to_type_formatter_instance(ast_model))
output_file.write(str(generator))
output_file.write('\n\n')
factories = []
for ast_model in ast_models:
if DisplayType.STRUCT == ast_model.display_type and ast_model.is_abstract:
factory_generator = FactoryClassFormatter(FactoryFormatter(factory_map, ast_model))
factories.append(str(factory_generator))
output_file.write('\n\n'.join(factories))
class Generator:
@staticmethod
def generate(ast_models, output):
print(f'python catbuffer generator called with output: {output}')
generate_files(ast_models, Path(output))
テスト出力
一度、テストでモデルを出力してみます。私の場合ですがgeneratorを編集しながら何度も出力し、自分の意図したものが仕上がるか確認していました。なお、ビルドにはlarkが必要です。
pip install lark
./scripts/run_catbuffer_generator.sh
/symbol/sdk/symbol_dart_sdk/lib/symbol/models.dart
ここに正しくファイルが出力されていればOKです。もちろんヘッダ以外の中身はpythonのままです。
AbstractTypeFormatter
まず、事前に編集したほうが良い物としてAbstractTypeFormatter
があります。とは言え、これは言語によっては変わらないかも。dartで編集したのは以下
class MethodDescriptor:
def __init__(self, method_name=None, arguments=None, body=None, result='', super=None, annotations=None):
self.method_name = method_name
self.arguments = arguments or []
self.super = super
self.body = body
self.result = result
self.annotations = annotations or []
メソッド用のディスクリプタで、関数名や引数、戻り値などを渡すためのクラスです。
dartではクラスを継承した際に基底クラスへ値を渡したりする際にsuperを使うので追加しました。
def get_interface(self):
# pylint: disable=no-self-use
return ''
interfacceを使いたかったのでこれも追加
TypeFormatter
/symbol/sdk/dart/generator/TypeFormatter.py
次にTypeFormatter
を編集します。これは、作成したい言語において、クラスの宣言、コンストラクタ、関数などの記法によって編集されます。
極力解説していきます。今回はdartに編集後を例とします。作成したい言語に置き換えてください。また、ここは完成ではなく進めながら編集していくと思います。
generate_method
@staticmethod
def generate_method(method_descriptor):
super = method_descriptor.super if method_descriptor.super else ''
arguments = ', '.join(method_descriptor.arguments)
if len(arguments) > 100:
arguments = '\n' + ',\n'.join(method_descriptor.arguments) + '\n'
if len(method_descriptor.arguments) > 1:
arguments = f'{{ {arguments}}}'
if not 'get ' in method_descriptor.method_name:
arguments = '(' + arguments + ')'
annotations = '\n'.join(method_descriptor.annotations + [''])
method_result = f'{method_descriptor.result} ' if method_descriptor.result else ''
body = f'{{\n{indent(method_descriptor.body)}}}' if method_descriptor.body else ';'
return f'{annotations}{method_result}{method_descriptor.method_name}{arguments} {super}{body}'
これはクラス内の関数の記法を定義します。
dartの関数は以下のように記述されます
static Amount deserialize(Uint8List payload) {
// ここにbody
}
そのためgenerate_method
の戻り値は
return f'{annotations}{modifier}{method_result}{method_descriptor.method_name}({arguments}){super}{body}'
これを展開すると上のようになります。
また、コンストラクタは下のように出力したかったのでそれに合わせて編集しています。
VrfProof({ ProofGamma? gamma, ProofVerificationHash? verificationHash, ProofScalar? scalar}) {
_gamma = gamma ?? ProofGamma();
_verificationHash = verificationHash ?? ProofVerificationHash();
_scalar = scalar ?? ProofScalar();
}
全て説明すると長くなりすぎるので、今後は簡単な説明とdartでのコードを記載していきます。
generate_class_header
クラスのヘッダー箇所です、interfaceなど継承したいクラスがあれば追加されます。ISerializableは全てのクラスが継承します。
def generate_class_header(self):
base_class = self.provider.get_base_class()
base_class = f' extends {base_class}' if base_class else ''
interface = self.provider.get_interface()
base_class += f' implements ISerializable{interface}' if interface else ' implements ISerializable'
header = f'class {self.provider.typename}{base_class} {{\n'
comment = ''
return header + indent(comment)
generate_class
先程のヘッダーを含みフィールドや関数を記述する箇所です。
ほぼ編集は不要かと思いますが、閉じカッコが要るかとかそういったことは注意してください。
def generate_class(self):
output = self.generate_class_header()
fields = self.provider.get_fields()
fields_output = ''
for field in fields:
fields_output += indent(field)
if fields_output:
output += fields_output + '\n'
methods = self.generate_methods()
output += '\n'.join(map(indent, methods))
output += '}\n' # class_footer
return output
generate_ctor
コンストラクタです
def generate_ctor(self):
method_descriptor = self.provider.get_ctor_descriptor()
if not method_descriptor:
return None
method_descriptor.method_name = self.provider.typename
return self.generate_method(method_descriptor)
generate_comparer
NEMにしか使いませんが比較用の関数
def generate_comparer(self):
method_descriptor = self.provider.get_comparer_descriptor()
if not method_descriptor:
return None
method_descriptor.method_name = 'comparer'
method_descriptor.arguments = []
method_descriptor.result = 'Tuple2'
return self.generate_method(method_descriptor)
generate_sort
モザイクなどsort用の関数
def generate_sort(self):
method_descriptor = self.provider.get_sort_descriptor()
if not method_descriptor:
return None
method_descriptor.method_name = 'sort'
method_descriptor.arguments = []
method_descriptor.result = 'void'
return self.generate_method(method_descriptor)
generate_deserializer
デシリアライザー関数。ISerializable(deserializeも強制)を継承しているのでannotationに@override
を追加。
def generate_deserializer(self):
# 'deserialize'
method_descriptor = self.provider.get_deserialize_descriptor()
method_descriptor.method_name = 'deserialize'
method_descriptor.arguments = ['Uint8List payload']
method_descriptor.result = self.provider.typename
method_descriptor.annotations = ['@override']
return self.generate_method(method_descriptor)
generate_serializer
シリアライザー関数。常に戻り値はUint8Listなので固定。ISerializableを継承しているのでannotationに@override
を追加。
def generate_serializer(self):
method_descriptor = self.provider.get_serialize_descriptor()
method_descriptor.method_name = 'serialize'
method_descriptor.result = 'Uint8List'
method_descriptor.annotations = ['@override']
return self.generate_method(method_descriptor)
generate_size
サイズを返すための関数。dartではgetにしました。
例えばトランザクションならペイロードのサイズ...Amountなら64bit整数なので8(SIZEというフィールドを持つのでそのまま返す)。ISerializableを継承しているのでannotationに@override
を追加。
def generate_size(self):
method_descriptor = self.provider.get_size_descriptor()
if not method_descriptor:
return None
method_descriptor.method_name = 'get size'
method_descriptor.arguments = []
method_descriptor.result = 'int'
method_descriptor.annotations = ['@override']
return self.generate_method(method_descriptor)
generate_getters, generate_setters
ゲッターとセッター
ここは別のフォーマッターで記述するから変更なし
generate_representation
文字列にして出力するための関数
def generate_representation(self):
method_descriptor = self.provider.get_str_descriptor()
if not method_descriptor:
return None
method_descriptor.method_name = 'toString'
method_descriptor.result = 'String'
method_descriptor.annotations = ['@override']
return self.generate_method(method_descriptor)
いちいち全部書きましたが、ここではその言語における記法で大枠を定義する程度です。
ここから、順にフォーマッターのタイプごとに編集していきます。
PodTypeFormatter
PodTypeFormatter
はIntタイプとArrayタイプに使用します。
例えば__init__.py
の最初のIntタイプとArrayタイプはAmountとUnresolvedAddressです。
class Amount(BaseValue):
SIZE = 8
def __init__(self, amount: int = 0):
super().__init__(self.SIZE, amount, Amount)
@classmethod
def deserialize(cls, payload: ByteString) -> Amount:
buffer = memoryview(payload)
return Amount(int.from_bytes(buffer[:8], byteorder='little', signed=False))
def serialize(self) -> bytes:
return self.value.to_bytes(8, byteorder='little', signed=False)
class UnresolvedAddress(ByteArray):
SIZE = 24
def __init__(self, unresolved_address: StrBytes = bytes(24)):
super().__init__(self.SIZE, unresolved_address, UnresolvedAddress)
@property
def size(self) -> int:
return 24
@classmethod
def deserialize(cls, payload: ByteString) -> UnresolvedAddress:
buffer = memoryview(payload)
return UnresolvedAddress(ArrayHelpers.get_bytes(buffer, 24))
def serialize(self) -> bytes:
return self.bytes
これをdartに書き換えてみます。
import '../BaseValue.dart';
import '../ByteArray.dart';
import 'dart:typed_data';
class Amount extends BaseValue implements ISerializable {
static const int SIZE = 8;
Amount([dynamic amount]) : super(SIZE, amount ?? 0);
@override
Amount deserialize(Uint8List payload) {
return Amount(bytesToInt(payload.sublist(0, 8), 8));
}
@override
Uint8List serialize() {
var buffer = Uint8List(SIZE);
buffer.setRange(0, 0 + 8, intToBytes(value, 8));
return buffer;
}
}
class UnresolvedAddress extends ByteArray implements ISerializable {
static const int SIZE = 24;
UnresolvedAddress([dynamic unresolvedAddress]) : super(SIZE, unresolvedAddress ?? Uint8List(24));
int get size {
return 24;
}
@override
UnresolvedAddress deserialize(Uint8List payload) {
payload = payload.sublist(0, SIZE);
return UnresolvedAddress(Uint8List.fromList(payload));
}
@override
Uint8List serialize() {
return bytes;
}
}
BaseValueとByteArrayという2つの基底クラスが必要なことが分かりました。これらをまず用意します。
import './utils/converter.dart';
class BaseValue {
final int size;
dynamic value;
final List<dynamic> _tag;
BaseValue(this.size, this.value, {dynamic tag})
: _tag = [tag] {
if (value is String) {
try {
if(size == 8) {
value = BigInt.parse(value);
} else {
value = int.parse(value);
}
} catch (_) {
tryHexString(value);
if(size == 8) {
value = BigInt.parse(value, radix: 16);
} else {
value = int.parse(value, radix: 16);
}
}
} else if (value is int && size == 8) {
value = BigInt.from(value).toUnsigned(64);
}
// check bounds
var bitSize = size * 8;
var upperBound = size == 8 ? BigInt.parse('18446744073709551615').toUnsigned(64) : bitmask(bitSize);
var lowerBound = size == 8 ? BigInt.from(0).toUnsigned(64) : 0;
if (value < lowerBound || value > upperBound) {
var valueRangeMessage = '$value must be in range [$lowerBound, $upperBound]';
throw ArgumentError('$valueRangeMessage for $size bytes');
}
}
@override
bool operator ==(Object other) {
if (other is BaseValue) {
return value == other.value && _tag == other._tag;
}
return false;
}
@override
int get hashCode => value.hashCode ^ _tag.hashCode;
@override
String toString() {
return '0x' + value.toRadixString(16).padLeft(size * 2, '0').toUpperCase();
}
bool get isDefault{
return value == 0;
}
}
int bitmask(int bitsNumber) {
return (1 << bitsNumber) - 1;
}
int unsignedToSigned(int value, int byteSize) {
var bitSize = byteSize * 8;
var mask = (1 << bitSize) - 1;
var signBit = 1 << (bitSize - 1);
if ((value & signBit) != 0) {
return value | ~mask;
} else {
return value & mask;
}
}
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'utils/converter.dart';
class ByteArray {
Uint8List bytes;
ByteArray(int fixedSize, dynamic arrayInput)
: bytes = Uint8List(fixedSize) {
var rawBytes = arrayInput;
if (rawBytes is String) {
try {
if (isHexString(rawBytes)) {
rawBytes = hex.decode(rawBytes);
} else {
rawBytes = stringToAddress(arrayInput);
}
} catch (e) {
throw ArgumentError('bytes was not a valid hex or address string');
}
}
if (fixedSize != rawBytes.length) {
throw RangeError('bytes was size ${rawBytes.length} but must be $fixedSize');
}
bytes = Uint8List.fromList(rawBytes);
}
@override
String toString() {
try {
return addressToString(bytes);
} catch(_) {
return hex.encode(bytes).toUpperCase();
}
}
bool get isDefault{
return bytes.every((byte) => byte == 0);
}
}
今回のSDKではByteArrayやBaseValueを継承したクラスのコンストラクタで引数をintでもhexでも両方受け入れるようにしたいので上記のようにしています。
// どちらでも良い
var f1 = FinalizationPoint('FFFFFFFF');
var f2 = FinalizationPoint(4294967295);
converterが必要なので作成します。この先使うものも全文掲載しておきます。
import 'dart:typed_data';
import 'dart:convert';
import 'package:base32/base32.dart';
import 'package:convert/convert.dart';
final Map<String, Map<String, int>> _constants = {
'sizes': {
'ripemd160': 20,
'symbolAddressDecoded': 24,
'nemAddressDecoded': 25,
'symbolAddressEncoded': 39,
'nemAddressEncoded': 40,
'key': 32,
'checksum': 3,
}
};
String intToHex(int num){
return num.toRadixString(16).padLeft(16, '0').toUpperCase();
}
String bytesToHex(Uint8List bytes) {
return hex.encode(bytes).toUpperCase();
}
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(hex.decode(hexString));
}
Uint8List bigintToUint8List(BigInt value) {
var result = Uint8List(8);
for (var i = 0; i < 8; i++) {
result[i] = (value >> (8 * i)).toUnsigned(8).toInt();
}
return result;
}
Uint8List intToBytes(dynamic value, int byteSize) {
var byteData = ByteData(byteSize);
switch (byteSize) {
case 1:
byteData.setUint8(0, value);
break;
case 2:
byteData.setUint16(0, value, Endian.little);
break;
case 4:
byteData.setUint32(0, value, Endian.little);
break;
case 8:
return bigintToUint8List(value);
default:
throw Exception('byteSize not supported');
}
return byteData.buffer.asUint8List();
}
BigInt uint8ListToBigInt(Uint8List data) {
var result = BigInt.from(0);
for (var i = 0; i < data.length; i++) {
result += BigInt.from(data[i]) << (8 * i);
}
return result;
}
dynamic bytesToInt(Uint8List input, int size) {
var byteData = ByteData.view(input.buffer, input.offsetInBytes, size);
switch (size) {
case 1:
return byteData.getUint8(0);
case 2:
return byteData.getUint16(0, Endian.little);
case 4:
return byteData.getUint32(0, Endian.little);
case 8:
return uint8ListToBigInt(input);
default:
throw Exception('byteSize not supported');
}
}
BigInt intToUnsignedInt(int i) {
var signedInt = BigInt.from(i);
BigInt unsignedInt;
if (signedInt < BigInt.zero) {
unsignedInt = signedInt + (BigInt.two.pow(64));
} else {
unsignedInt = signedInt;
}
return unsignedInt;
}
Uint8List stringToAddress(String encoded) {
if (_constants['sizes']!['symbolAddressEncoded'] == encoded.length) {
var bytes = base32.decode(encoded + 'A');
return Uint8List.fromList(bytes.sublist(0, _constants['sizes']!['symbolAddressDecoded']));
}
if (_constants['sizes']!['nemAddressEncoded'] == encoded.length) {
return utf8ToBytes(encoded);
}
throw Exception('$encoded does not represent a valid encoded address');
}
String addressToString(Uint8List decoded) {
if (_constants['sizes']!['symbolAddressDecoded'] == decoded.length) {
var padded = Uint8List(_constants['sizes']!['symbolAddressDecoded']! + 1);
padded.setRange(0, decoded.length, decoded);
return base32.encode(padded).substring(0, _constants['sizes']!['symbolAddressEncoded']);
}
if (_constants['sizes']!['nemAddressDecoded'] == decoded.length) {
return base32.encode(decoded);
}
throw Exception('Bytes to Hex function is not implemented yet. It does not represent a valid decoded address');
}
bool isHexString(String value) {
final hexPattern = RegExp(r'^[0-9a-fA-F]+$', caseSensitive: false);
return hexPattern.hasMatch(value);
}
void tryHexString(String value) {
if (!isHexString(value)) {
throw ArgumentError('value was not a valid hex string');
}
}
String utf8ToHex(String input) {
List<int> bytes = utf8.encode(input);
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join().toUpperCase();
}
Uint8List utf8ToBytes(String input) {
return utf8.encode(input);
}
interfaceとしてISerializableも使います。
import 'dart:typed_data';
interface class ISerializable {
Uint8List serialize(){
throw UnimplementedError('unimplement deserialize');
}
dynamic deserialize(Uint8List payload){
throw UnimplementedError('unimplement deserialize');
}
int get size{
throw UnimplementedError('unimplement deserialize');
}
}
さて、ヘッダでインポートすべき物が分かった(あくまでも現時点で)のでgeneratorを編集しておきます。
with open(output_directory / 'models.dart', 'w', encoding='utf8', newline='\n') as output_file:
output_file.write(
'''import '../BaseValue.dart';
import '../ByteArray.dart';
import '../models/ISerializable.dart';
import '../utils/converter.dart';
import 'dart:typed_data';
'''
それではPodTypeFormatterを編集していきましょう
@property
def field_name(self):
return f'this._{self.printer.name}'
実は使わないのですがフィールド名のルールはdartでは_(アンダースコア)をつけることでprivateなフィールドになるということなのでこのようにしておきます
get_fields
def get_fields(self):
return [f'static const int SIZE = {self.pod.size};']
PodTypeではフィールドはSIZEしか存在しないのでdartの記法にしたがってこのようにします。これはこのクラスの値のサイズです。IntObjectでは1(8bit), 2(16bit), 4(32bit), 8(64bit)の4種類しかありませんがAraryObjectの場合でもcatbufferに記述されているサイズがself.pod.sizeとして出力されます。
get_base_class
def get_base_class(self):
return 'ByteArray' if self._is_array else 'BaseValue'
基底クラスです
get_ctor_descriptor
def get_ctor_descriptor(self):
variable_name = self.printer.name
super = f': super(SIZE, {variable_name} ?? {self.printer.get_default_value()})'
arguments = [f'[dynamic? {variable_name}]']
return MethodDescriptor(super=super, arguments=arguments)
コンストラクタの宣言以外の箇所です。先程TypeFormatterではガワを用意しましたがこれは中身です。Dartではsuperを使って基底クラスへ引数を渡すのでこのようにしています。PodTypeではコンストラクタの中は空です。
self.printer.get_type()
の箇所はこのあとprinterを編集します。
ただし、variable_nameはこのままだとunresolved_address
のようなアンダースコアを使った記法になります。dartではキャメルケースが一般的なようなので追加します。printerも先に少しいじっておきます。
CAMEL_CASE_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
...
def lang_field_name(name):
return SNAKE_CASE_PATTERN.sub(_to_upper, name)
from .name_formatting import lang_field_name, fix_size_name, underline_name
...
class Printer:
def __init__(self, descriptor, name):
self.descriptor = descriptor
# printer.name is 'fixed' field name
self.name = fix_name(lang_field_name(name or underline_name(self.descriptor.name)))
言語によって変わると思います。
get_deserialize_descriptor
def get_deserialize_descriptor(self):
if self._is_array:
body = 'payload = payload.sublist(0, SIZE);\n'
body += f'return {self.typename}({self.printer.load("payload")});'
return MethodDescriptor(body=body)
body = f'return {self.typename}({self.printer.load("payload")});'
return MethodDescriptor(body=body)
デシリアライザーの中身です。self._is_array
によってIntObjectかArrayObjectを判別しています。
self.printer.load
の箇所はこのあとprinterを編集します。
get_serialize_descriptor
def get_serialize_descriptor(self):
body = 'var buffer = Uint8List(SIZE);\n'
body += f'{self.printer.store("value", 0)};\n'
body += 'return buffer;'
if self._is_array:
return MethodDescriptor(body='return bytes;')
return MethodDescriptor(body=body)
self._is_array
によってIntObjectかArrayObjectを判別しています。loadと同じくstoreはこの後printerを編集します。
get_size_descriptor
def get_size_descriptor(self):
if not self._is_array:
return None
body = f'return {self.pod.size}\n'
return MethodDescriptor(body=body)
ArrayObjectのみサイズを返す関数が必要です
printers
フォーマッターから呼び出すprinterですが、今回はPodTypeFormatterで使用する箇所のみ編集しておきます。今後、他にも触っていきますが、流れに沿って随時編集していきます。
IntタイプにはIntPrinter、ArrayタイプにはArrayPrinterです。
IntPrinter
get_type
def get_type(self):
return 'BigInt' if 8 == self.descriptor.size else 'int'
先程PodTypeFormatterのコンストラクタの引数の型で使用しました。
get_default_value
def get_default_value(self):
return 'BigInt.from(0)' if 8 == self.descriptor.size else '0'
Intタイプのobjectのデフォルト値です。BigIntを使うのでサイズによって切り分けます。
load
def load(self, buffer_name='buffer'):
data_size = self.get_size()
return f'bytesToInt({buffer_name}.sublist(0, {data_size}), {data_size})'
デシリアライザーで使います。
今後のために補足しておきます。ここ重要です。今後はいちいち説明しませんが、ここを理解しておくと捗ります。
先程PodTypeFormatterで
def get_deserialize_descriptor(self):
if self._is_array:
body = 'payload = payload.sublist(0, SIZE);\n'
body += f'return {self.typename}({self.printer.load("payload")});'
return MethodDescriptor(body=body)
body = f'return {self.typename}({self.printer.load("payload")});'
return MethodDescriptor(body=body)
のようにしましたよね。
buffer_nameにはloadの引数であるpayloadが入るので例えばAmountならsizeは8なのでload関数では
bytesToInt(payload.sublist(0, 8), 8)
が返されます。これがformatterで受け取ると
return Amount(bytesToInt(payload.sublist(0, 8), 8));
になるわけです。
つまりこれをbodyとしたメソッドを作成します。
body = 'return Amount(bytesToInt(payload.sublist(0, 8), 8));'
さらにTypeFormatterで定めた
def generate_deserializer(self):
# 'deserialize'
method_descriptor = self.provider.get_deserialize_descriptor()
method_descriptor.method_name = 'deserialize'
method_descriptor.arguments = ['Uint8List payload']
method_descriptor.result = self.provider.typename
method_descriptor.annotations = ['@override']
return self.generate_method(method_descriptor)
によって必要な情報を追加し、generate_methodを経由すると
@override
Amount deserialize(Uint8List payload) {
return Amount(bytesToInt(payload.sublist(0, 8), 8));
}
がmodelsに出力されます。意図したものと同じものが出力されました。
store
def store(self, field_name, pos):
data_size = self.get_size()
return f'buffer.setRange({pos}, {pos} + {data_size}, intToBytes({field_name}, {data_size}))'
toString
@staticmethod
def to_string(field_name, size):
return f'0x${{intToHex({field_name})}}'
シリアライザーの戻り値です。storeとloadはIntタイプのオブジェクトだけではなく、StructタイプオブジェクトでのデシリアライザーやシリアライザーのIntタイプの変数にも使うのでそのあたりの意識が必要になります。
ただ最初はなかなか理解しづらいので、ビルドしながら調整していくと思います。
ArrayPrinter
get_type
@staticmethod
def get_type():
return 'Uint8List'
get_default_value
def get_default_value(self):
size = self.descriptor.size
if isinstance(size, str):
return 'Uint8List(0)'
return f'Uint8List({self.get_size()})'
size
def get_size(self):
size = self.descriptor.size
if isinstance(size, str):
return f'{self.name}.lengthInBytes'
return size
load
def load(self):
return 'Uint8List.fromList(payload)'
store
@staticmethod
def store(field_name, pos):
return f'buffer.setRange(currentPos, currentPos + {field_name}.lengthInBytes, {field_name})'
toString
@staticmethod
def to_string(field_name):
return f'${{bytesToHex({field_name})}}'
IntPrinterと基本は同じなので特に解説しません。
ここまで来たら一旦IntObjectとArrayObjectが正しく出力されるか見てください。
./scripts/run_catbuffer_generator.sh
うまく出力されたらOKです
EnumTypeFormatter
次はenumです。enumにはLinkActionのようなEnumとビット演算を利用してFlagとして活用する2タイプがあります。pythonはこんな感じ
class LinkAction(Enum):
UNLINK = 0
LINK = 1
@property
def size(self) -> int:
return 1
@classmethod
def deserialize(cls, payload: ByteString) -> LinkAction:
buffer = memoryview(payload)
return LinkAction(int.from_bytes(buffer[:1], byteorder='little', signed=False))
def serialize(self) -> bytes:
buffer = bytes()
buffer += self.value.to_bytes(1, byteorder='little', signed=False)
return buffer
class MosaicFlags(Flag):
NONE = 0
SUPPLY_MUTABLE = 1
TRANSFERABLE = 2
RESTRICTABLE = 4
REVOKABLE = 8
@property
def size(self) -> int:
return 1
@classmethod
def deserialize(cls, payload: ByteString) -> MosaicFlags:
buffer = memoryview(payload)
return MosaicFlags(int.from_bytes(buffer[:1], byteorder='little', signed=False))
def serialize(self) -> bytes:
buffer = bytes()
buffer += self.value.to_bytes(1, byteorder='little', signed=False)
return buffer
dartではenumにメソッドを持たせることはできないのでenumライクなclassにします。(拡張メソッドという案もあるが)
仕上がりは以下のようにしたい。ここで気づいたけどpythonにはtoStringのoverrideが無いのでjavascriptを参考にした。言語によってはjsを編集したほうが楽な場合もある、というかそっちのほうが多いかもしれない。。。
注意点としては、flagタイプはvalueが8bit整数(256以下)と16bit(65536以下)の2パターンあるので、そのあたりはサイズで調整する。
class LinkAction implements ISerializable {
static final UNLINK = LinkAction(0);
static final LINK = LinkAction(1);
int value = 0;
static final _flags = {
0: 'UNLINK',
1: 'LINK',
};
LinkAction([int? _value]) {
value = _value ?? 0;
}
@override
int get size {
return 1;
}
@override
LinkAction deserialize(Uint8List payload) {
var byteData = ByteData.sublistView(payload);
return LinkAction(byteData.getUint8(0));
}
@override
Uint8List serialize() {
var byteData = ByteData(1)..setUint8(0, value);
return byteData.buffer.asUint8List();
}
@override
String toString() {
return 'LinkAction.${_flags[value]}';
}
}
class MosaicFlags implements ISerializable {
static final NONE = MosaicFlags(0);
static final SUPPLY_MUTABLE = MosaicFlags(1);
static final TRANSFERABLE = MosaicFlags(2);
static final RESTRICTABLE = MosaicFlags(4);
static final REVOKABLE = MosaicFlags(8);
int value = 0;
static final _flags = {
0: 'NONE',
1: 'SUPPLY_MUTABLE',
2: 'TRANSFERABLE',
4: 'RESTRICTABLE',
8: 'REVOKABLE',
};
MosaicFlags([int? _value]) {
value = _value ?? 0;
}
@override
int get size {
return 1;
}
@override
MosaicFlags deserialize(Uint8List payload) {
var byteData = ByteData.sublistView(payload);
return MosaicFlags(byteData.getUint8(0));
}
@override
Uint8List serialize() {
var byteData = ByteData(1)..setUint8(0, value);
return byteData.buffer.asUint8List();
}
@override
String toString() {
if (value == 0) {
return 'NONE';
}
return _flags.entries
.where((e) => value & e.key != 0)
.map((e) => e.value)
.join(' | ');
}
}
class AccountRestrictionFlags implements ISerializable {
static final ADDRESS = AccountRestrictionFlags(1);
static final MOSAIC_ID = AccountRestrictionFlags(2);
static final TRANSACTION_TYPE = AccountRestrictionFlags(4);
static final OUTGOING = AccountRestrictionFlags(16384);
static final BLOCK = AccountRestrictionFlags(32768);
int value = 0;
static final _flags = {
1: 'ADDRESS',
2: 'MOSAIC_ID',
4: 'TRANSACTION_TYPE',
16384: 'OUTGOING',
32768: 'BLOCK',
};
AccountRestrictionFlags([int? _value]) {
value = _value ?? 1;
}
@override
int get size {
return 2;
}
@override
AccountRestrictionFlags deserialize(Uint8List payload) {
var byteData = ByteData.sublistView(payload);
return AccountRestrictionFlags(byteData.getUint16(0, Endian.little));
}
@override
Uint8List serialize() {
var byteData = ByteData(2)..setUint16(0, value, Endian.little);
return byteData.buffer.asUint8List();
}
@override
String toString() {
return _flags.entries
.where((e) => value & e.key != 0)
.map((e) => e.value)
.join(' | ');
}
}
編集箇所が多いので全文記載してコメントで簡単に解説します
from .AbstractTypeFormatter import AbstractTypeFormatter, MethodDescriptor
from .printers import IntPrinter
# indentを行いたいので以下追加
from .format import indent
class EnumTypeFormatter(AbstractTypeFormatter):
def __init__(self, ast_model):
super().__init__()
self.enum_type = ast_model
# dartでenumは継承しないので以下削除
# self.base_type = 'Flag' if self.enum_type.is_bitwise else 'Enum'
self.int_printer = IntPrinter(self.enum_type)
@property
def typename(self):
return self.enum_type.name
# 同じくdartでenumは継承しないので以下削除
# def get_base_class(self):
# return f'({self.base_type})'
# サイズによってgetIntやsetIntの関数名を変えたいので以下追加
def get_bit_size(self):
if self.enum_type.size == 1:
return '8'
elif self.enum_type.size == 2:
return '16'
elif self.enum_type.size == 4:
return '32'
elif self.enum_type.size == 8:
return '64'
# key=>valueのflagsを追加したいので変更
def get_fields(self):
fields = list(
map(
lambda e: f'static final {e.name} = {self.typename}({e.value});\n',
self.enum_type.values,
)
)
fields.append('\n')
fields.append('int value = 0;\n')
fields.append('\n')
flags = 'static final _flags = {\n'
flags += indent(''.join(
map(
lambda e: f'{e.value}: \'{e.name}\',\n',
self.enum_type.values,
)
))
flags += '};\n'
fields.append(flags)
return fields
# コンストラクタは引数と関数名、valueへのセット
def get_ctor_descriptor(self):
method_name = f'{self.typename}'
arguments = ['[int? _value]']
body = f'value = _value ?? {self.enum_type.values[0].value};\n'
return MethodDescriptor(method_name=method_name, body=body, arguments=arguments, is_enum_ctor=True)
# 先程定義したget_bit_sizeを使ってgetIntをgetInt8やgetInt16などに可変
def get_deserialize_descriptor(self):
body = 'var byteData = ByteData.sublistView(payload);\n'
if self.get_bit_size() == '8':
body += f'return {self.enum_type.name}(byteData.getUint8(0));'
else:
body += f'return {self.enum_type.name}(byteData.getUint{self.get_bit_size()}(0, Endian.little));'
return MethodDescriptor(body=body)
# デシリアライズと基本は同じ
def get_serialize_descriptor(self):
endian = ', Endian.little' if not self.get_bit_size() == '8' else ''
body = f'var byteData = ByteData({self.enum_type.size})..setUint{self.get_bit_size()}(0, value{endian});\n'
body += f'return byteData.buffer.asUint8List();'
return MethodDescriptor(body=body)
def get_size_descriptor(self):
body = f'return {self.enum_type.size};\n'
return MethodDescriptor(body=body)
def get_str_descriptor(self):
body = ''
# flagsがbitwiseでない場合は単一のenumを文字列で返す
if not self.enum_type.is_bitwise:
body = f'return \'{self.typename}.${{_flags[value]}}\';'
return MethodDescriptor(body=body)
# flagsがbitwiseの場合は複数のflagを文字列で返す
if any(e.value == 0 for e in self.enum_type.values):
result = indent('return \'NONE\';')
body += f'if (value == 0) {{\n{result}}}\n'
body += '''return _flags.entries
.where((e) => value & e.key != 0)
.map((e) => e.value)
.join(' | ');'''
return MethodDescriptor(body=body)
ここまで来たらまた出力して意図したとおりかどうか確かめる。なお、途中何度も出力はしている。
かなり長くなってきたため、Qiitaの編集画面が重くなってきました。一旦終了し、StructTypeFormatterからは別記事にします。
続きが書けました