数据序列化和持久化

蚂蚁区块链智能合约平台分别提供了基础数据类型的序列化和用户自定义数据类型的持久化方法。

基础数据序列化

可序列化的数据可以使用 pack() 函数序列化为字节串(即std::string),并且可以使用unpack()函数将对应的字节串反序列化为原来的值。

 std::string buff = pack("hello"s);
 std::string str = unpack<std::string>(buff); // str的值将会是"hello"

 std::string int_buff = pack(1234);
 int n = unpack<int>(int_buff); // n 的值会是1234

可序列化数据类型

可序列化数据类型分有两大类:合约平台自身支持的可序列化数据类型用户自定义的可序列化数据类型

合约平台自身支持的可序列化数据类型

合约平台自身支持的可序列化数据类型包括:

  • int8_t

  • uint8_t

  • int16_t

  • uint16_t

  • int32_t

  • uint32_t

  • int64_t

  • uint64_t

  • bool

  • std::string

  • Identity

  • std::vector<T>(“T”仅限于上面的类型,如 std::vector<int8_t>

    注意:pack 函数不支持 C 风格字符串作为参数,因为 C 风格字符串末尾有一个隐含的终止符 ‘\0’,在序列化和反序列化时可能会导致歧义。当您需要表达字符串常量时,可以使用 C++17 中的 string_literals 操作符。

在下面的例子中,错误地把 C 风格字符串输入给 pack 函数会导致合约编译不通过:

const char* s1 = "hello";
std::string b1 = pack(s1); // wrong

const char s1[] = "hello";
std::string b2 = pack(s2); // wrong

std::string b3 = pack("hello"); // wrong

string s4 = "hello";
std::string b4 = pack(s4); // correct

string s5 = "hello"s;
std::string b5 = pack(s5); // correct

std::string b6 = pack("hello"s); // correct

pack 函数按照下面的规则进行序列化。

编码表(字节序均为小端)

类型

编码

字节数

编码示例(编码后数据以 16 进制展示)

C++ SDK 类型

Java SDK/JS SDK 类型

bool

原码

1

false=>00

true=>01

bool

Bool

uint8

原码

1

123=>7B

uint8_t

Uint8

uint16

原码

2

12345=>39,30

uint16_t

Uint16

uint32

原码

4

1234567890=>D2,02,96,49

uint32_t

Uint32

uint64

原码

8

1234567890123ull=>CB,04,FB,71,1F,01,00,00

uint64_t

Uint64

int8

补码

1

-123=>85

int8_t

Int8

int16

补码

2

-12345=>C7,CF

int16_t

Int16

int32

补码

4

-1234567890=>2E,FD,69,B6

int32_t

Int32

int64

补码

8

-1234567890123=>35,FB,04,8E,E0,FE,FF,FF

int64_t

Int64

任何指针

编译时报错。

T *

字符串,每个元素为字符类型

元素字符用 utf-8 编码

先入以 LEB128 编码的 uint32 表达元素个数,然后遍历放入元素。

"hello世界"=>0B,68,65,6C,6C,6F,E4,B8,96,E7,95,8C

std::string

Utf8String

数组,元素必为上述内置整型或string类型

先入以 LEB128 编码的 uint32 表达元素个数,然后遍历放入元素。

int32 数组:

{10, 20, 30}=>03,0A,00,00,00,14,00,00,00,1E,00,00,00

string 数组:

{"hello", "smart", "world"}=>03,05,68,65,6C,6C,6F,05,73,6D,61,72,74,05,77,6F,72,6C,64

std::vector

DynamicArray

字节流,操作接口和字符串一致

先入以 LEB128 编码的 uint32 表达元素个数,然后遍历放入元素。

std::string:

{10, 20, 30}=>03,0A,14,1E

std::string

std::string

用户自定义的可序列化数据类型

可以使用 SERIALIZE 宏将自定义数据类型(struct/class)序列化。

  SERIALIZE(结构体名, (成员变量1)(成员变量2)...(成员变量n))

示例:

struct Foo {
  int32_t x;
  std::string y;
  bool z;
  int32_t tmp_value; //如果SERIALIZE中不写tmp_value, 则tmp_value不参与序列化
  SERIALIZE(Foo, (y)(x)(z))
};

Foo f;
f.x = -1234567890;
f.y = "hello世界";
f.z = true;

基于 编码表 中对基本类型的编码,Foo 类型的 f 变量的编码相当于按照 SERIALIZE 指定的顺序依次编码各成员。未声明的成员不参与编码,如上例中,即按 y—>x—>z 进行顺次编码,首尾相接,结果为 0B,68,65,6C,6C,6F,E4,B8,96,E7,95,8C,2E,FD,69,B6,01

使用 Schema 持久化数据

简介

开发智能合约时,经常需要将某些数据持久化存储,或者从持久化存储中读取数据内容。为方便您以比较友好地方式实现数据持久化,以及支持复杂的数据结构(比如 map 类型嵌套),智能合约平台引入基于 Schema 的存储系统。您在开发智能合约时,首先通过 Schema 描述存储对象数据结构以及各数据结构间的逻辑关系,然后在智能合约中使用根据 Schema 生成的 API 来操作数据对象,智能合约在运行时会自动将数据对象的修改持久化存储。

Schema 语法

Schema 的语法定义与 C 语言非常类似,如果您有 protobuf 或其他 IDL 的使用经验,则理解 Schema 语法会比较容易。下面是一个 Schema 示例:

// example IDL file

namespace MyContract.Sample;

//table中如果需要定义 map/map_iterable,需提前声明
attribute "map";
attribute "map_iterable";

table Transfer {
  count:int = 1;
  accounts:[Account](map:"v2");
  iterable_accounts:[Account](map_iterable:"v2");
}

table Account {
  age:short = 18; //定义字段后,设置默认值为18
  gender:short = 1;
  create:int(deprecated); //deprecated 表示该字段已废弃,相当于删除
  orders:[Order](map:"v2"); //带 'map'属性,该字段为 map
  banalce:Balance; //成员为Balance对象
  friends:[Friend]; //数组
}

table Order {
  id:string;
  sender:string;
  receive:string;
  amount:int = 0;
}

table Balance {
  rmb:int = 0;
  usd:int = 0;
}

table Friend {
  name:string;
  age:int = 0;
}

//通过root_type指定root类型table,其它所有table都直接或间接属于该table
root_type Transfer;

下面简要分析 Schema 语法。

table

Schema 中的 table 对应 C++ 中的 Class,您可在 table 中定义任意数目的不同类型字段,每个字段包含类型、名字、默认值(可选,如果未设置,默认为0/NULL),同时字段还有附加属性,也就是字段后面括号中的内容,例如: accounts:[Account](map:"v2") 中的 map,表示该字段为 map,v2表示该类型的版本号。

table 中的字段类型可为基础类型、数组、map、string 及其他 table 类型。关于 Schema 支持的数据类型,参见下面的 数据类型 章节。

此外,Schema 语法支持在已有的 table 中添加新字段,废弃已有字段。为确保 Schema 能够保持前向、后向兼容数据,在修改 Schema 时您必须遵循以下两点:

  • 只能在 table 的尾部追加新字段。

  • 不能在 Schema 中直接删除字段。如果不再使用某字段,只需在该字段的属性中标明 deprecated 即可。

满足以上条件后,Schema 中对存储数据的访问可实现前向、后向数据兼容。

数据类型

Schema 中 table 字段类型支持基础类型、数组、map、string 等。其中基础类型的长度如下:

1 byte:  bool
2 bytes: short (int16), ushort (uint16)
4 bytes: int (int32), uint (uint32)
8 bytes: long (int64), ulong (uint64)

链版本从V2.23开始,MyBuffer提供Integer128/UInteger128类型。使用该类型必须引入mybuffer/fbs/types.fbs依赖,比如:

include "mybuffer/fbs/types.fbs";

table TAccount {
    f_int128: Integer128;
    f_uint128: UInteger128;
}
root_type TAccount;

Integer128/UInteger128类型,使用get_value()和set_value()来读取字段的值,以及对该字段赋值。

数组:通过 [type] 定义,其元素成员 type 必须为 table 类型,示例如下:

friends:[Friend];

map/map_iterable 的定义形式与数组类似,仍然通过 [type] 定义,但在属性中标明 mapmap_iterable,以表示其类型。map/map_iterable的值为 type 类型,且 type 必须为 table 类型;map/map_iterable 的 key 类型为 string。以下是两个例子,例子当中的“v2”表示 map/map_iterable 类型的版本号。

重要

数组与map的type必须为table类型,主要是为了良好的扩展性,在接下来的版本中会考虑支持数组与map的type为基本类型。

v2版本示例:

accounts:[Account](map:"v2");
iterable_accounts:[Account](map_iterable:"v2");
说明

从0.10.2.18版本开始,推荐您使用 map/map_iterable 的 v2 版本。新的版本更为安全和灵活,原有版本在某些重名或者嵌套使用的场景下,会触发编译器错误。原有的版本会继续支持,但是后续新功能的增加将以 v2 为主。

原有版本示例:

accounts:[Account](map);
iterable_accounts:[Account](map_iterable);
说明

v2 提供的接口和旧版完全一致。您只需在字段声明的时候添加一个”v2”的版本标识即可。 在编写全新的 schema 时,或者添加新的字段时,推荐使用 v2, 以获得更好的安全性,灵活性,以及持续的功能升级。 旧版本的字段不能升级到 v2。

命名空间

Schema 中可通过 namespace 来声明命名空间,这样生成的所有 C++ 代码会包裹在该命名空间中。

namespace MyContract.Sample;

如果在 Schema 中定义了以上 namespace,为便于编写代码,可在合约的 cpp 文件代码中使用 using namespace MyContract::Sample;,或者在使用到的变量及函数前添加命名空间前缀。

Root 类型

Schema 中最后通过root_type来声明根类型 table。

root_type Transfer;

根类型的 table 将会作为合约中访问存储数据的入口,合约必须也只能从根 table 对象开始访问遍历各数据对象信息,也就是说,其他 table 类型最终都是依附于根 table 而存在。

另外,需要注意的是,访问存储数据过程中,根类对象生命周期必须要贯穿于整个使用范围,由于 table 对象间可能存在嵌套关系,在访问子对象成员时,同样要确保父对象仍存活。

Schema 生成的 C++ 代码

Schema 中定义的每一个 table 都对应 C++ 中的一个类,C++ 中生成的类名与 Schema 中的 table 名称基本一致,但带 M 后缀。例如上述示例中的 table Account,生成的 C++ 类名为 AccountM

同时,为方便使用,会对每个生成的 C++ 类都会生成带 MPtr 后缀的类型,该类型是相关类的智能指针缩写,即 AccountMPtr = std::unique_ptr<AccountM>,由该智能指针负责管理对象的生命周期。

Schema 生成 C++ 规则

在根据 Schema 生成 C++ 代码的过程中,table 中的不同类型字段都会在 C++ 类中生成相应 API 来操作该字段。出于性能考虑,智能合约平台会对不同类型的字段,生成不同形式的 API 接口,基本生成原则如下:

  • 若字段 X 为基本类型,如 int,则提供:

    • set_x(_x):设置 x 的值。

    • x():读取 x 的值。

  • 若字段 X 为 string,则提供:

    • set_x(_x):设置 x 的值。

    • get_x():读取 x 的值。

  • 若字段 X 为 table 对象,则提供:

    • get_x():返回该对象指针。

  • 若字段 X 为数组类型且元素为 table 对象,则提供:

    • get_x():返回该数组容器指针。

  • 若字段 X 为 map/map_iterable 类型且 val 为 table 对象,则提供:

    • get_x():返回该map/map_iterable指针。

  • 若字段X为Integer128/UInteger128,提供

    • get_x():返回该字段Integer128MPtr/UInteger128MPtr类型的指针

对于Integer128/UInteger128类型,我们提供以下的方法调用:

  • get_value():返回该字段的值,返回值类型为 int128_t/uint128_t;

  • set_value(T x):设置该字段的值,其中参数类型 T 为 int128_t/uint128_t;

对于数组容器,我们提供以下的方法调用:

- size() :返回数组大小。
- get_element(int index) :获得某下标位置处的对象指针。
- append_element() :在尾部追加一个元素,返回对象指针。
- insert_element(int index) :在某一下标索引处插入对象,返回对象指针。
- delete_element(int index) :删除某一下标位置处的对象元素。
- clear() :清空数组对象。

对于map/map_iterable容器,我们提供以下的方法调用:

- add_element(string key) :添加一个键值对,返回值对象指针;若键存在,直接覆盖原来键值对。
- get_element(string key, bool revert_on_failure=true) :通过键查找值,若存在,返回值对象指针;

否则,在revert_on_failure的值为true时将抛出异常(Revert),为false时返回nullptr。

- has_element(string key) :判断某一键是否存在,若存在返回true,否则返回false。
- delete_element(string key) :删除某一键值。
重要

对于 table X 对象来说,所有通过 get_x() 系列接口返回的值均为对象智能指针,即 std::unique_ptr<XM>。这也就意味着,当该智能指针维护的变量生命周期结束时,智能指针负责销毁并析构对象,如果对象被修改过,对象析构时将自动序列化并持久存储。

对于数组字段,限制数组长度最大为 1024,超过该长度后继续追加元素会报错。

对于 string 字段,限制最大长度为 2K,即 2048 个字节,string 字段超过该阈值后会导致赋值失败。

说明

支持根据实际需求调整 string 字段的最大长度限制,但请留意,增加长度将导致合约的读写速度下降,建议设置时不要超过500KB的长度。

上面主要介绍了不同字段类型生成的 API 形式,未标明返回值与参数,如需了解完整的 API 信息,参见 Schema 生成的 C++ API

另外,对于 Schema 中定义的 map 类型来说,如果需要值为基本类型,考虑到性能与扩展性,建议将该基本类型封装在 table 中,因为 table 支持任意添加字段,具备良好的兼容性。以 table A 中需要定义字段 usd [int](map:"v2") 为例,usd 为 map 类型,代表某一账户的金钱余额,值为 int。经过封装改造,您可在 Schema 中定义如下 table:

table Balance {
  usd:int = 0;
}

然后在 table A 中定义如下字段:

balance [Balance](map:"v2");

在合约代码中,可通过 BalanceM 提供的 API 来读取或修改 usd 字段值。随着合约不断迭代,可往 table BalanceM 中添加更多字段来代表不同余额,比如人民币、欧元等等。也就是说,在 Schema 中一切皆为 table。

Schema 生成的 C++ API

Schema 生成 C++ 规则 章节简要分析了 Schema 中 table 定义的各类型字段生成 API 接口以及相关容器API接口的规则,下面列出了 Schema 中table各类型字段生成的完整API信息。当字段类型为table/vector/map,或者Integer128/UInteger128时,只提供get_x()接口,返回table/vector/map类型,或者Integer128/UInteger128类型的指针,之后可通过指针进一步操作其字段或者元素。

T

提供的接口

作用

参数

返回值

基础类型

bool

short

ushort

int

uint

long

ulong

T

get_x()

获得成员 x 的值。

返回成员 x 的值。

bool

set_x(T _x)

设置成员 x 的值。

参数 _x,代表需要设置的值。

返回类型为 bool,true 代表成功;false 代表失败

string

string

get_x()

获得成员 x 的值。

返回成员 x 的值。

bool

set_x(string _x)

设置成员 x 的值。

参数 _x,代表需要设置的值。

返回类型为 bool,true 代表成功;false 代表失败。

Integer128 UInteger128

TMPtr get_x()

获得指向该类型的指针,其中 TMPtr 为 Integer128MPtr 或 UInteger128MPtr

返回指向该类型的指针:TMPtr,其中 TMPtr 为 Integer128MPtr 或 UInteger128MPtr

Table T

TMPtr get_x()

获得指向该table的指针。

返回指向该table的指针:TMPtr。

[ T ]

TMVectorPtr

get_x()

获得数组容器的指针。

返回数组容器的指针:TMVectorPtr。

[ T ](map:"v2")

TMMapPtr

get_x()

获取该map容器指针。

返回类型为map容器的指针:TMMapPtr。

[ T ](map_iterable:"v2")

TMMapIterablePtr

get_x()

获取该map_iterable容器指针。

返回类型为map_iterable容器的指针:TMMapIterablePtr。

以下表格列出了Integer128/UInteger128的完整API信息。

提供的接口

作用

参数

返回值

Tget_value()

获得该字段的值

返回该字段的值:类型 T 为 int128_t 或 uint128_t

bool set_value(T x)

设置该字段的值

用户指定的值,类型 T 为 int128_t 或 uint128_t

设置是否成功

以下表格列出了数组容器的完整API信息。其中get_element()/append_element()/insert_element()接口返回成员类型指针TMPtr,可通过返回的指针调用字段接口为各字段赋值或读取字段内容。数组最大长度为1024,当数组元素超过1024时,调用append_element()/insert_element()接口会抛出异常(Revert),代表失败。

提供的接口

作用

参数

返回值

TMPtr

get_element(int i)

获得数组下标i处的元素

参数i,代表数组下标

返回数组成员类型的指针:TMPtr

TMPtr

append_element()

数组尾部追加一个元素

返回数组成员类型的指针:TMPtr

TMPtr

insert_element(int i)

数组下标i处插入一个元素

参数i,代表数组下标

返回数组成员类型的指针:TMPtr

int

delete_element(int i)

删除数组下标i处的元素

参数i,代表数组下标

返回类型为int,0代表成功;其他代表失败

uint32_t

size()

获得数组长度

返回数组长度

int

clear()

清空数组

返回类型为int,0代表成功;其他代表失败

以下表格列出了map容器的完整API信息。其中add_element()/get_element()接口返回值类型的指针TMPtr,接下来通过指针调用各字段接口为字段赋值或读取字段内容。

提供的接口

作用

参数

返回值

TMPtr

add_element(string key)

map中添加一对key/val

参数key,代表插入map中的key

返回类型为map中val类型的指针:TMPtr

TMPtr

get_element(string key, bool revert_on_failure=true)

根据键查找值

参数为key,代表map中的键

返回类型为map中值类型的指针:TMPtr

bool

has_element(string key)

确认键是否存在

参数key,代表map中的键

返回类型为bool,true代表存在;false代表不存在

int

delete_element(string key)

删除该键

参数key,代表map中的键

返回类型为int,0代表成功;其他代表失败

以下表格列出了map_iterable容器的完整API信息。

提供的接口

作用

参数

返回值

TMPtr

add_element(string key)

map_iterable中添加一对key/val

参数key,代表插入map_iterable中的key

返回类型为map_iterable中val类型的指针:TMPtr

TMPtr

get_element(string key, bool revert_on_failure=true)

根据键查找值

参数为key,代表map_iterable中的键

返回类型为map_iterable中值类型的指针:TMPtr

bool

has_element(string key)

确认键是否存在

参数key,代表map_iterable中的键

返回类型为bool,true代表存在;false代表不存在

int

delete_element(string key)

删除该键

参数key,代表map_iterable中的键

返回类型为int,0代表成功;其他代表失败

uint32_t

size()

获得map_iterable长度

返回map_iterable长度

TMMapKeyIterator

get_map_key_iterator()

获取map_iterable关键字迭代器

返回类型是map_iterable的关键字迭代器

以下表格列出了MapKeyIterator的完整API信息。

提供的接口

作用

参数

返回值

bool

valid()

判断当前的迭代器是否有效

返回类型为bool,true代表有效,false代表无效

std::string

operator*()

获取当前迭代器指向的关键字

返回类型为字符串

TMMapKeyIterator&

operator++()

移动迭代器指向下一个关键字

返回类型为迭代器本身的引用

通过学习上表中的 API 说明,您在 Schema 中定义 table 字段时,基本可以确定在合约中如何访问该字段信息。

Schema 持久化存储的初始化

在智能合约里面使用 Schema 生成的 API 之前,必须对持久化存储环境进行初始化。需要在合约的 Init() 方法里调用根据 Schema 编译生成的 InitRoot() 函数,然后在合约部署时显式调用 Init 进行初始化。调用方式如下:

说明

InitRoot不能多次调用,否则会抛出异常(Revert)。

INTERFACE Init() {
    InitRoot();
}

另外,每次使用schema编译生成的接口访问持久化存储的时候,都必须首先获得Schema中定义的根类型table,参见页面下方使用my++.sh脚本自动化编译的getTransferM()调用。可以考虑将这个处理放在合约类的构造函数里,这样每次调用合约的时候,都会自动获得存储的根节点,不需要在每个合约函数里反复写这样的处理。

合约开发

前面主要介绍了引入 Schema 机制的目的、Schema 语法以及根据 Schema 生成C++ API 的规则。下面将会介绍如何在实际的合约开发中使用 Schema。

引入基于 Schema 的存储系统后,合约开发与平常的开发并无二样,遵守合约开发指南即可。目前,合约开发的方式有以下两种:

下面将分别对这两种合约开发方式进行说明。

使用脚本编译合约

下载 mycdt 安装包(参考 C++ 合约编译工具 文档下载相应的安装包),即可在本地开发合约。本地开发合约时,我们提供了以下两种方法可供选择:

  • 使用命令行手动编译:合约cpp文件和schema定义在不同的文件中。

  • 使用my++.sh脚本自动化编译:合约cpp文件和schema文件可以定义在一个文件内。

使用命令行手动编译

如果为了灵活性,用户可选择手动编译合约,手动编译时需要遵循以下步骤。

步骤1:首先为合约存储定义Schema,并将Schema内容保存为以.fbs结尾的Schema文件。例如,将以下Schema内容保存为transfer.fbs

//Schema Demo
namespace MyContract.Sample;

attribute "map";
attribute "map_iterable";

table Transfer {
  count:int = 1;
  accounts:[Account](map:"v2");
  iterable_accounts:[Account](map_iterable:"v2");
}

table Account {
  age:short = 18;
  gender:short = 1;
  create:int(deprecated);
  orders:[Order](map:"v2");
  balance:Balance;
  friends:[Friend];
}

table Order {
  id:string;
  sender:string;
  receive:string;
  amount:int = 0;
}

table Balance {
  rmb:int = 0;
  usd:int = 0;
}

table Friend {
  name:string;
  age:int = 0;
}

root_type Transfer;

步骤2:使用mycdt中的myflatc.sh工具将Schema生成C++API。

myflatc.sh ./ transfer.fbs
说明

第一个参数./代表生成的c++文件存储目录;第二个参数transfer.fbs代表schema文件。

步骤3:在合约cpp文件中通过#include "transfer_ant_generated.h"将头文件引入即可,并将合约cpp保存为transfer_contract.cpp。以下是合约cpp的文件示例,将下列代码保存为transfer_contract.cpp文件。

//简单转账合约示例,仅供演示
//合约源文件
#include <mychainlib/contract.h>

//只需包含上步中生成的xxx_ant_generated.h即可
#include "transfer_ant_generated.h"

using namespace mychain;
using namespace MyContract::Sample;//如果schema中没有定义namespace可省略

class demo:public Contract {
public:
    //使用成员变量保存指向存储root的指针
    TransferMPtr m_ptransfer;

    demo() {
        //在构造函数里获得Schema中定义的根类型table:Transfer, 之后可以直接使用了
        m_ptransfer = GetTransferM();
    }
    INTERFACE void Init() {
        InitRoot();
    }
    INTERFACE void writedemo() {
        //若空指针,代表异常出错,合约异常退出
        if(!m_ptransfer) {
            Revert("error");
        }
        //设置transfer的count值
        m_ptransfer->set_count(88);

        //map中新增一个账户,key为"btc",返回对象指针
        AccountMMapPtr paccount_map = m_ptransfer->get_accounts();
        AccountMPtr paccount = paccount_map->add_element("btc");
        //如果对象指针为NULL,返回值异常,合约退出
        if(!paccount) {
            Revert("error");
        }
        paccount->set_age(19);
        paccount->set_gender(1);
        //获得Balance对象
        BalanceMPtr pbalance = paccount->get_balance();
        //如果对象指针为NULL,返回值异常,合约退出
        if(!pbalance) {
            Revert("error");
        }
        pbalance->set_rmb(199);
        pbalance->set_usd(1000);

        //账户中orders字段为map,新增加一个kv,key为“order1”,返回对象指针,当该智能指针维护的
        //order对象生命周期结束后,该kv内容自动序列化并持久存储
        OrderMMapPtr porder_map = paccount->get_orders();
        OrderMPtr porder = porder_map->add_element("order1");
        porder->set_sender("user1");
        porder->set_receive("user2");
        porder->set_amount(8899);

        //friends为数组,数组尾部追加一个对象元素,返回对象指针
        FriendMVectorPtr pfriend_vec = paccount->get_friends();
        FriendMPtr pfriend = pfriend_vec->append_element();
        pfriend->set_name("myant");
        pfriend->set_age(88);

        //调用智能指针的reset()后,paccount管理的Account对象`btc`生命周期结束,若该对象内容修改
        //过,则对象析构时自动序列化存储;
        paccount.reset();

        //账户“btc”中添加一个order,key为 `order2`,返回对象指针,porder中维护的旧对象(order1)
        //生命周期结束,析构时自动序列化存储
        porder = porder_map->add_element("order2");
        porder->set_sender("user3");
        porder->set_receive("user4");
        porder->set_amount(88888);

        //合约中新增加一个账户"eth",返回对象指针,paccount维护的旧对象("btc")生命周期结束,析构时
        //自动序列化存储
        paccount = paccount_map->add_element("eth");
        paccount->set_age(22);
        paccount->set_gender(0);
        pbalance = paccount->get_balance();
        pbalance->set_rmb(299);
        pbalance->set_usd(4000);

        //账户"eth"中增加一个order,key为"order1",返回对象指针,porder维护的旧对像生命周期结
        //束,析构时自动序列化存储
        porder_map = paccount->get_orders();
        porder = porder_map->add_element("order1");
        porder->set_sender("user1");
        porder->set_receive("user2");
        porder->set_amount(88999);

        //账户"eth"中增加一个order,key为"order2",返回对象指针,porder维护的旧对像生命周期结
        //束,析构时自动序列化存储
        porder = porder_map->add_element("order2");
        porder->set_sender("user5");
        porder->set_receive("user6");
        porder->set_amount(8899988);

        //friends数组追加一个元素,返回对象指针,pfriend维护的旧对象生命周期结束,析构时自动序列化
        //存储
        pfriend = pfriend_vec->append_element();
        pfriend->set_name("lingling");
        pfriend->set_age(87);

        //iterable_accounts是map_iterable类型,增加两个元素“btc”和“eth”
        AccountMMapIterablePtr paccount_mi = m_ptransfer->get_iterable_accounts();
        AccountMPtr paccount1 = paccount_mi->add_element("btc");
        AccountMPtr paccount2 = paccount_mi->add_element("eth");
    }

    INTERFACE void readdemo() {
        //若空指针,代表异常出错,合约异常退出
        if(!m_ptransfer) {
            Revert("error");
        }
        //获取Root对象中的count字段值
        int count = m_ptransfer->get_count();
        //通过"btc"获取账户内容,若存在,返回对象指针;否则将抛出异常(Revert)
        AccountMMapPtr paccount_map = m_ptransfer->get_accounts();
        AccountMPtr paccount = paccount_map->get_element("btc");
        if(!paccount) {
            Revert("error");
        }
        //访问账户内的各字段值
        int age = paccount->get_age();
        int gender = paccount->get_gender();

        //test statement: will output to log file
        println("age=%d", age);

        //通过"order1"获得账单信息,若存在,返回对象指针;否则将抛出异常(Revert)
        OrderMMapPtr porder_map = paccount->get_orders();
        OrderMPtr porder = porder_map->get_element("order1");
        if(!porder) {
            Revert("error");
        }
        //获得order对象的各字段值
        std::string sender = porder->get_sender();
        std::string receive = porder->get_receive();
        //通过下标0获得friends数组中的第一个元素,返回对象指针,若下标越界,将抛出异常(Revert)
        FriendMVectorPtr pfriend_vec = paccount->get_friends();
        FriendMPtr pfriend = pfriend_vec->get_element(0);
        if(!pfriend) {
            Revert("error");
        }

        auto name = pfriend->get_name();
        age= pfriend->get_age();
        //获得账户中balance对象,若存在,返回对象指针;若不存在,返回的指针指向空内容
        BalanceMPtr pbalance = paccount->get_balance();
        if(!pbalance) {
            Revert("error");
        }
        int rmb= pbalance->get_rmb();
        int usd= pbalance->get_usd();

        //获得iterable_accounts的对象,并利用它的迭代器遍历所有元素:“btc”和“eth”
        AccountMMapIterablePtr paccount_mi = m_ptransfer->get_iterable_accounts();
        AccountMMapKeyIterator iaccount = paccount_mi->get_map_key_iterator();
        for (; iaccount.valid(); ++iaccount) {
            std::string key = *iaccount;
            //在这里使用key
        }

        /*
         * 其它操作
         */
    }
};
INTERFACE_EXPORT(demo, (Init) (writedemo) (readdemo))

步骤4:调用my++工具将合约cpp编译成wasm字节码。

my++ transfer_contract.cpp -o transfer.wasm

至此,本地开发合约并手动编译的过程全部结束。

使用my++.sh脚本自动化编译

本地开发合约时,为方便使用,智能合约平台提供 my++.sh 脚本,将整个编译流程封装起来,只需使用该脚本就能编译合约代码。为使用 my++.sh 脚本,您需要将合约 Schema 内容与合约 cpp 放在一起。智能合约平台提供 STORAGE_SCHEMA_BEGINSTORAGE_SCHEMA_END 两个宏标记,用这两个宏分别作为开头与结尾将 Schema 内容包裹起来并嵌在合约 cpp 文件开始处即可。

重要

STORAGE_SCHEMA_BEGINSTORAGE_SCHEMA_END 两个宏包裹的 Schema 内容必须位于合约 cpp 文件的开头。

下面是将 Schema 内容完整嵌入合约 cpp 文件的示例:

//简单转账合约示例,仅供演示
//合约源文件

#include <mychainlib/contract.h>
using namespace mychain;

STORAGE_SCHEMA_BEGIN
namespace MyContract.Sample;

attribute "map";
attribute "map_iterable";

table Transfer {
  count:int = 1;
  accounts:[Account](map:"v2");
  iterable_accounts:[Account](map_iterable:"v2");
}
table Account {
  age:short = 18;
  gender:short = 1;

  create:int(deprecated);

  orders:[Order](map:"v2");
  balance:Balance;
  friends:[Friend];
}
table Order {
  id:string;
  sender:string;
  receive:string;
  amount:int = 0;
}
table Balance {
  rmb:int = 0;
  usd:int = 0;
}
table Friend {
  name:string;
  age:int = 0;
}
root_type Transfer;

STORAGE_SCHEMA_END

using namespace MyContract::Sample;//如果schema中没有定义namespace可省略
class demo:public Contract {
public:
    TransferMPtr ptransfer;

    demo () {
        //首先获得Schema中定义的根类型table : Transfer
        ptransfer = GetTransferM();
    }
    INTERFACE void Init() {
        InitRoot(); //初始化schema持久化存储环境
    }
    INTERFACE void writedemo() {
        //若返回空指针,代表异常出错,合约异常退出
        if(!ptransfer) {
              Revert("error");
        }
        //设置transfer的count值
        ptransfer->set_count(88);

        //map中新增一个账户,key为"btc",返回对象指针
        AccountMMapPtr paccount_map = m_ptransfer->get_accounts();
        AccountMPtr paccount = paccount_map->add_element("btc");
        //如果对象指针为NULL,返回值异常,合约退出
        if(!paccount) {
              Revert("error");
        }
        paccount->set_age(18);
        paccount->set_gender(1);
        //获得Balance对象
        BalanceMPtr pbalance = paccount->get_balance();
        //如果对象指针为NULL,返回值异常,合约退出
        if(!pbalance) {
              Revert("error");
        }
        pbalance->set_rmb(199);
        pbalance->set_usd(1000);

        //账户中orders字段为map,新增加一个kv,key为“order1”,返回对象指针,当该智能指针维护的
        //order对象生命周期结束后,该kv内容自动序列化并持久存储
        OrderMMapPtr porder_map = paccount->get_orders();
        OrderMPtr porder = porder_map->add_element("order1");
        porder->set_sender("user1");
        porder->set_receive("user2");
        porder->set_amount(8899);

        //friends为数组,数组尾部追加一个对象元素,返回对象指针
        FriendMVectorPtr pfriend_vec = paccount->get_friends();
        FriendMPtr pfriend = pfriend_vec->append_element();
        pfriend->set_name("myant");
        pfriend->set_age(88);

        //调用智能指针的reset()后,paccount管理的Account对象`btc`生命周期结束,若该对象内容修改
        //过,则对象析构时自动序列化存储;
        paccount.reset();

        //账户“btc”中添加一个order,key为 `order2`,返回对象指针,porder中维护的旧对象(order1)
        //生命周期结束,析构时自动序列化存储
        porder = porder_map->add_element("order2");
        porder->set_sender("user3");
        porder->set_receive("user4");
        porder->set_amount(88888);

        //合约中新增加一个账户"eth",返回对象指针,paccount维护的旧对象("btc")生命周期结束,析构时
        //自动序列化存储
        paccount = paccount_map->add_element("eth");
        paccount->set_age(22);
        paccount->set_gender(0);
        pbalance = paccount->get_balance();
        pbalance->set_rmb(299);
        pbalance->set_usd(4000);

        //账户"eth"中增加一个order,key为"order1",返回对象指针,porder维护的旧对像生命周期结
        //束,析构时自动序列化存储
        porder_map = paccount->get_orders();
        porder = porder_map->add_element("order1");
        porder->set_sender("user1");
        porder->set_receive("user2");
        porder->set_amount(88999);

        //账户"eth"中增加一个order,key为"order2",返回对象指针,porder维护的旧对像生命周期结
        //束,析构时自动序列化存储
        porder = porder_map->add_element("order2");
        porder->set_sender("user5");
        porder->set_receive("user6");
        porder->set_amount(8899988);

        //friends数组追加一个元素,返回对象指针,pfriend维护的旧对象生命周期结束,析构时自动序列化
        //存储
        pfriend = pfriend_vec->append_element();
        pfriend->set_name("lingling");
        pfriend->set_age(87);

        //iterable_accounts是map_iterable类型,增加两个元素“btc”和“eth”
        AccountMMapIterablePtr paccount_mi = m_ptransfer->get_interable_accounts();
        AccountMPtr paccount1 = paccount_mi->add_element("btc");
        AccountMPtr paccount2 = paccount_mi->add_element("eth");

        //整体函数结束,最后顶层的Root类型ptransfer生命周期结束,析构时自动序列化存储
        //要保证Root对象的生命周期在整个函数中有效
    }

    INTERFACE void readdemo() {
        //获取Root对象中的count字段值
        int count = ptransfer->get_count();
        //通过"btc"获取账户内容,若存在,返回对象指针;否则将抛出异常(Revert)
        AccountMMapPtr paccount_map = m_ptransfer->get_accounts();
        AccountMPtr paccount = paccount_map->get_element("btc");
        if(!paccount) {
            Revert("error");
        }
        //访问账户内的各字段值
        int age = paccount->get_age();
        int gender = paccount->get_gender();

        //通过"order1"获得账单信息,若存在,返回对象指针;否则将抛出异常(Revert)
        OrderMMapPtr porder_map = paccount->get_orders();
        OrderMPtr porder = porder_map->get_element("order1");
        if(!porder) {
             Revert("error");
        }
        //获得order对象的各字段值
        std::string sender = porder->get_sender();
        std::string receive = porder->get_receive();
        //通过下标0获得friends数组中的第一个元素,返回对象指针,若下标越界,将抛出异常(Revert)
        FriendMVectorPtr pfriend_vec = paccount->get_friends();
        FriendMPtr pfriend = pfriend_vec->get_element(0);
        if(!pfriend) {
             Revert("error");
        }

        auto name = pfriend->get_name();
        age= pfriend->get_age();
        //获得账户中balance对象,若存在,返回对象指针;若不存在,返回的指针指向空内容
        BalanceMPtr pbalance = paccount->get_balance();
        if(!pbalance) {
             Revert("error");
        }
        int rmb= pbalance->get_rmb();
        int usd= pbalance->get_usd();

        //获得iterable_accounts的对象,并利用它的迭代器遍历所有元素:“btc”和“eth”
        AccountMMapIterablePtr paccount_mi = m_ptransfer->get_interable_accounts();
        AccountMMapKeyIterator iaccount = paccount_mi->get_map_key_iterator();
        for (; iaccount.valid(); ++iaccount) {
            std::string key = *iaccount;
            //在这里使用key
        }

        /*
         * 其它操作
        */

        //函数结束后,Root类型对象ptransfer生命周期结束,ptransfer的生命周期贯穿整个函数
    }
};
INTERFACE_EXPORT(demo, (Init) (writedemo) (readdemo))

假如将以上合约代码保存为 transfer_contract.cpp 文件,在命令行中调用如下命令即可将合约编译成字节码:

my++.sh transfer_contract.cpp -o transfer.wasm

使用在线 Cloud IDE 开发合约

使用 BaaS 在线 Cloud IDE 开发合约时,由于目前 Cloud IDE 只能在一个文件中编辑,所以您需要将合约 Schema 内容与合约代码放在一起。

您仍可以用 STORAGE_SCHEMA_BEGINSTORAGE_SCHEMA_END 这两个宏分别作为开头与结尾将 Schema 内容包裹起来,放在合约 cpp 文件的开始处。有关如何将合约 Schema 与合约代码放在一起,参见 使用脚本编译合约

在线编辑合约文件完成后,点击 提交 即可开始编译合约。