QRust(二)数据类型

QRust支持的数据类型可分为两类:基本类型、集合类型。这些数据类型可作为函数参数、返回值或struct的字段,在Qt和Rust之间传递。

基本类型

Rust端Qt端
boolbool
i8qint8
i16qint16
i32qint32
i64qint64
u8quint8
u16quint16
u32quint32
u64quint64
f32float
f64double
StringQString
T structT struct

集合类型

Rust端Qt端
Vec<bool>QList<bool>
Vec<i8>QList<qint8>
Vec<i16>QList<qint16>
Vec<i32>QList<qint32>
Vec<i64>QList<qint64>
Vec<u8>QList<quint8>
Vec<u16>QList<quint16>
Vec<u32>QList<quint32>
Vec<u64>QList<quint64>
Vec<f32>QList<float>
Vec<f64>QList<double>
Vec<String>QList<QString>
Vec<T struct>QList<T struct>
HashMap<i32, bool>QHash<qint32, bool>
HashMap<i32, i8>QHash<qint32, qint8>
HashMap<i32, i16>QHash<qint32, qint16>
HashMap<i32, i32>QHash<qint32, qint32>
HashMap<i32, i64>QHash<qint32, qint64>
HashMap<i32, u8>QHash<qint32, quint8>
HashMap<i32, u16>QHash<qint32, quint16>
HashMap<i32, u32>QHash<qint32, quint32>
HashMap<i32, u64>QHash<qint32, quint64>
HashMap<i32, f32>QHash<qint32, float>
HashMap<i32, f64>QHash<qint32, double>
HashMap<i32, String>QHash<qint32, String>
HashMap<i32, T struct>QHash<qint32, T struct>
HashMap<String, bool>QHash<QString, bool>
HashMap<String, i8>QHash<QString, qint8>
HashMap<String, i16>QHash<QString, qint16>
HashMap<String, i32>QHash<QString, qint32>
HashMap<String, i64>QHash<QString, qint64>
HashMap<String, u8>QHash<QString, quint8>
HashMap<String, u16>QHash<QString, quint16>
HashMap<String, u32>QHash<QString, quint32>
HashMap<String, u64>QHash<QString, quint64>
HashMap<String, f32>QHash<QString, float>
HashMap<String, f64>QHash<QString, double>
HashMap<String, String>QHash<QString, QString>
HashMap<String, T struct>QHash<QString, T struct>

注意上面表格中以q开始的通常是一些C++类型的别名,比如qint32就是int。在使用QRust编程时变量定义为qint32还是int是没有区别的,下面列出了这些别名的对应。

typedefC++原类型
qint8signed char
qint16signed short
qint32signed int
qint64long long int
quint8unsigned char
quint16unsigned short
quint32unsigned int
quint64unsigned long long int

数据包结构

以上类型的数据在Qt和Rust之间传递时被封装为一种二进制包结构,基本类型的包结构如下:

基本数据类型包结构以一个字节的tag标记开始,tag的数值表示这是哪种类型,后面的值部分根据类型不同占用字节长度也有区分,比如i8和u8占用1个字节,u16和i16占用两个字节。比较特殊的是string类型,在tag后紧跟4个字节描述字符串的长度,在长度单元后是字符串的值(utf-8字节向量,没有’\0’结尾)。

集合类型的包结构稍显复杂一些,但不难理解。

list包结构:

list包的tag根据其包含的数据类型不同而有变化,范围50~99。tag后跟4字节长度单元,描述list包体的长度。包体部分是一个接一个的元素,元素的结构和基本数据包结构一样。

以int为key的Hash包结构:

hash包的tag根据其包含的数据类型变化,范围100~149,和list一样tag后跟4字节长度单元。包体中每个元素由两部分组成,因为key是int类型所以结构和基础包结构中的i32相同,值部分也是可参照基础包结构的描述。

以字符串为key的Hash包结构:

以字符串为key的tag取值范围150~199,和以int为key的hash包结构基本相同,只是元素key有变化。

struct类型:

QRust支持自定义的struct类型,struct在应用场景中可以等同为对象,支持struct的自动化(也可能称之为半自动化)转换是QRust的靓点之一。

使用Qrust进行struct数据类型在Qt和Rust之间传递时,有以下注意点:

1)字段名称、数量双方必须一致,struct名称可以不一样。

2)Rust端需要对struct加上序列化和反序列化的宏标记,例如:

[derive(Serialize, Deserialize, Clone, Debug)]
pub struct A {
    a: bool,
    v: Vec,
}

3)Qt端需要对struct进行QMetaObject的改造,例如:

struct A{
  Q_GADGET
  Q_PROPERTY(bool a MEMBER a);
  Q_PROPERTY(QList v MEMBER v);
public:
  bool a;
  QList v;
};

在Rust端必须给struct添加Serialize和Deserialize宏,在Qt端需要对struct进行以下扩展:

  • 第一需要添加 Q_GADGET 宏标志,来支持Qt的元对象编程。
  • 第二为每个字段添加 Q_PROPERTY 宏,格式: Q_PROPERTY(类型 字段名 MEMBER 字段名),Q_PROPERTY 宏支持struct的字段遍历、读写、以及类型推导。
  • 第三在字段前添加 public:标志,因为Q_PROPERTY 宏执行以 private: 结束,将导致所有字段变为私有的,添加 public: 将强制改回来。

struct的包结构是所有类型中最复杂的,但也不难理解:

struct的tag标志是24(10进制),tag后的一个字节描述struct名称的长度,后面跟struct名称字符串向量。名称后的一个字节描述字段的数量(这点限制了字段最多255个),再后面4个字节描述包体的长度,包体部分是一个一个的字段。字段结构分为两个部分,字段名和字段值。字段名符合基本类型String的定义,字段值可参考基本类型或集合类型的定义。

注意struct字段并没有全部支持基本类型和集合类型,需要去除这几个:

  • struct
  • Vec<struct>
  • HashMap<i32, struct>
  • HashMap<String, struct>

类型不足的原因和解决方法

struct的字段不能再定义为另一个struct类型,既struct不能嵌套,例如你不能在QRust中使用如下的struct定义:

struct A{
struct B b;
}

同样的原因在集合类型中也不能嵌套其他集合,你不能定义这样的变量:

QList<QHash<int, bool>> a;

集合类型和struct都属于复杂类型,QRust不支持复杂类型嵌套的原因和序列化和反序列化技术有关,序列化技术在Rust端使用了规范框架库serde,在Qt端对struct的处理使用了QMetaObject,这两种技术都不支持嵌套或不完全支持。

对于Rust和Qt这种静态语言在序列化处理过程中离不开模板和宏,例如在struct的序列化过程中进行类型推导,编译预处理时就可能生成大量代码而增加源代码的长度,如果允许复杂类型的嵌套,代码长度可能会指数级增加导致出现问题。有关C++模板和Rust宏的编译预处理比较复杂,这里不展开讨论,有兴趣可浏览相关的书籍资料。

但struct嵌套是经常遇到的场景,比如下面的struct结构:

struct User{
  qint32  id;
  QString name;
  struct Address address; 
};

在用户对象中包含地址对象是自然而然地,如果不支持嵌套怎么来解决这个问题呢?QRust的解决办法是扁平化,将User和Address分成两个参数传递,函数定义可做修改:

add_user(user);  //原定义user中包含address

改为:

add_user(user, address);  //扁平化的

函数入参比较容易进行扁平化,如果从数据库查询,即函数返回值怎样处理呢,能同时返回user和address吗?答案是可以,QRust针对性的做了扩展,可以同时返回多个返回值。

Rust端的函数定义,返回tuple结构可包含多个返回值:

pub fn get_user(user_id: i32) -> (User, Address)

反序列化时也可对多个返回值进行处理:

let (user, address) = get_user(user_id); 
let mut pack = Vec::new();
pack.extend(ser::to_pack(&user)?);  //序列化第一个返回值
pack.extend(ser::to_pack(&address)?);   //序列化第二个返回值
let pack_multi = ser::to_pack_multi(pack);  //两个返回值组包在一起
Ok(Some(pack_multi))

在OnTheSSH软件中,经常使用这种多返回值的方式,例如系统监控要同时获得CPU、内存、磁盘、网络、端口等多种结构信息。

这个问题再扩展一些,比如获得全部用户时,扁平化可以这样处理:

pub fn get_all_users() -> (Vec<User>, Vec<Address>)
pub fn get_all_users() -> (Vec<User>, HashMap<i32, Address>)

第一种方式需要在Address中定义一个user_id的字段,来关联地址是属于哪一个用户的,第二种则直接用key进行关联。这也是在QRust的Hash类型中为什么会同时存在int和string两种类型的key,因为int和string在数据库中最常用作主键,使用频率都很高。

QRust对函数参数和多返回值的最大数量,限制在255个。

空和异常

在业务系统中空和异常是经常遇到的特殊结果,比如get_user(id)时用户已被删除,或者发送命令而网络不可达时。在OnTheSSH软件中是利用多返回值来处理空和异常的,第一个返回值是一个特殊结构:

struct RetState
{
    bool    state;          
    qint32  err_code;       
};

字段state表示函数执行的状态,如果为真表示函数执行成功,你再从第二个返回值开始获得函数的结果,反之如果为假,就不必获取后续的函数结果了,这时应该查看err_code字段是哪种错误的编码。

序列化和反序列化函数

使用serde框架让Rust有很好的类型推导能力,所以序列化和反序列化时有统一from_pack和to_pack函数。Qt端目前还做不到,每种类型都需要对应独立的函数,下表列出了所有的序列化和反序列化对应关系:

类型序列化函数反序列化函数
boolpack_boolupack_bool
qint8pack_i8upack_i8
qint16pack_i16upack_i16
qint32pack_i32upack_i32
qint64pack_i64upack_i64
quint8pack_u8upack_u8
quint16pack_u16upack_u16
quint32pack_u32upack_u32
quint64pack_u64upack_u64
floatpack_f32upack_f32
doublepack_f64upack_f64
QStringpack_strupack_str
structpack_structupack_struct
QList<bool>pcak_list_boolupcak_list_bool
QList<qint8>pcak_list_i8upcak_list_i8
QList<qint16>pcak_list_i16upcak_list_i16
QList<qint32>pcak_list_i32upcak_list_i32
QList<qint64>pcak_list_i64upcak_list_i64
QList<quint8>pcak_list_u8upcak_list_u8
QList<quint16>pcak_list_u16upcak_list_u16
QList<quint32>pcak_list_u32upcak_list_u32
QList<quint64>pcak_list_u64upcak_list_u64
QList<float>pcak_list_f32upcak_list_f32
QList<double>pcak_list_f64upcak_list_f64
QList<QString>pcak_list_strupcak_list_str
QList<T struct>pcak_list_structupcak_list_struct
QHash<qint32, bool>pack_hi_boolupack_hi_bool
QHash<qint32, qint8>pack_hi_i8upack_hi_i8
QHash<qint32, qint16>pack_hi_i16upack_hi_i16
QHash<qint32, qint32>pack_hi_i32upack_hi_i32
QHash<qint32, qint64>pack_hi_i64upack_hi_i64
QHash<qint32, quint8>pack_hi_u8upack_hi_u8
QHash<qint32, quint16>pack_hi_u16upack_hi_u16
QHash<qint32, quint32>pack_hi_u32upack_hi_u32
QHash<qint32, quint64>pack_hi_u64upack_hi_u64
QHash<qint32, float>pack_hi_f32upack_hi_f32
QHash<qint32, double>pack_hi_f64upack_hi_f64
QHash<qint32, QString>pack_hi_strupack_hi_str
QHash<qint32, T struct>pack_hi_structupack_hi_struct
QHash<QString, bool>pack_hs_boolupack_hs_bool
QHash<QString, qint8>pack_hs_i8upack_hs_i8
QHash<QString, qint16>pack_hs_i16upack_hs_i16
QHash<QString, qint32>pack_hs_i32upack_hs_i32
QHash<QString, qint64>pack_hs_i64upack_hs_i64
QHash<QString, quint8>pack_hs_u8upack_hs_u8
QHash<QString, quint16>pack_hs_u16upack_hs_u16
QHash<QString, quint32>pack_hs_u32upack_hs_u32
QHash<QString, quint64>pack_hs_u64upack_hs_u64
QHash<QString, float>pack_hs_f32upack_hs_f32
QHash<QString, double>pack_hs_f64upack_hs_f64
QHash<QString, QString>pack_hs_strupack_hs_str
QHash<QString, T struct>pack_hs_structupack_hs_struct