LoginSignup
16
6

Symbol 各言語のSDKを作成する -その1-

Last updated at Posted at 2024-01-21

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を起動するためのシェルスクリプトを編集します。出力フォルダの指定、ファイルの拡張子の変更程度です。

symbol/sdk/dart/scripts/run_catbuffer_generator.sh
#!/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

/symbol/sdk/dart/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つの基底クラスが必要なことが分かりました。これらをまず用意します。

/symbol/sdk/symbol_dart_sdk/bin/BaseValue.dart
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;
  }
}
/symbol/sdk/symbol_dart_sdk/bin/ByteArray.dart
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が必要なので作成します。この先使うものも全文掲載しておきます。

/symbol/sdk/symbol_dart_sdk/bin/utils/converter.dart
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も使います。

/symbol/sdk/dart/symbol_sdk/lib/models/ISerializable.dart
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を編集しておきます。

/symbol/sdk/dart/generator/Generator.py
	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を編集していきましょう

/symbol/sdk/dart/generator/PodTypeFormatter.py
@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も先に少しいじっておきます。

/symbol/sdk/dart/generator/name_formatting.py
CAMEL_CASE_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
...
def lang_field_name(name):
	return SNAKE_CASE_PATTERN.sub(_to_upper, name)
/symbol/sdk/dart/generator/printers.py
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(' | ');
	}
}

編集箇所が多いので全文記載してコメントで簡単に解説します

/symbol/sdk/dart/generator/EnumTypeFormatter.py
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からは別記事にします。

続きが書けました

16
6
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6