前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >本体技术视点 | 虚拟机中引用性动态语言对象模型思考

本体技术视点 | 虚拟机中引用性动态语言对象模型思考

作者头像
本体Ontology
发布2019-12-05 17:43:41
4110
发布2019-12-05 17:43:41
举报

1

引言

Ontology 的 NeoVM 虚拟机新增加了 DCALL、HAS_KEY、KEYS 以及 VALUES 等几条新的指令。因此,基于 NeoVM 的引用性动态语言对象的设计理论上可行,这可使得当前语言的支持能更接近原生语义。

对象模型设计的必要性

Ontology NeoVM 对用户暴露的对象语义有4种,分别是 bytearray、array、struct 和 map。当前 Python、Go、C#编译器的实现都是直接复用这4种对象语义,这样一来就产生了几个问题:

  • 首先,高级语言的基本对象往往不止这几种对象语义,就会出现对象语义多对一的情况。 不同对象的运算有不同的行为,导致的后果是必须要牺牲其中一种对象的语义。
  • 其次, 高级语言对象对应的底层对象,语义不一定是完全对等的。

综上,需要设计一个较通用的对象模型框架,以适应不同语言的语义对象,满足多语言智能合约的支持。

以 Python 为例,Python 是引用性的动态类型语言,在编译时获取的信息量较少。当前 Ontology 的 Neptune 编译器已基本实现 Python 的运算逻辑及控制逻辑。静态类型的语言如 Go和C#等,在编译时即可处理类型检查、对象语义区分等问题。但对于 Python 这类动态类型的语言,如果没有较完备的对象内存模型,其表达能力是有限的,不能精确区分不同对象的语义。

本文基于 Ontology NeoVM 提出一种引用性动态语言内存模型的设计,作为升级重构 Neptune 以及 Go 编译器和更精确实现其它语言编译器的理论分析。

2

对象模型

理论上,底层指令的语义模型需要足够简单抽象,才能满足不同类型语言语义的实现。而且很难有一套指令架构,能满足所有语言语义的运行要求。所以绝大多数高级语言都是重新定义特定的语义模型,构建在特定虚拟机之上运行。而相对底层的语言如 Rust,C 和 C++等则直接编译后运行在 CPU 上。

内存对象模型最佳的方式是不直接使用 Ontology NeoVM 的内置语义对象,而是重新根据语言特性设计其对象模型,更精准的语义对开发者更友好。但是重写设计对象语义的代价在于,相同的逻辑实现,会产生数倍于当前实现编译生成的字节码,且编译器的实现会更复杂。

当前按照 Python 一切皆对象的语义设计,所有对象使用 map 或者 array 实现。为简化表达,这里假设使用 map 实现对象。map 的第一个 key 为内置的__type__或用编码表示,编译器会检查属性 key 字段不属于系统预留字段。在 Python 中,基本对象类型为:Number、string、List、Tuple、Set、Dict;基本运算符为[](subscript), +/-/*/%///以及 le 等。而这些运算符号是相应对象的成员属性。在运行时,可通过 type 字段,对运算符做不同的语义区分。同样的, 函数也是对象。各对象可用如下结构表示:

{"__type__":"function", "offset":xxx}
{"__type__":"int", "__value__":value}
{"__type__":"string", "__value__":value}
{"__type__":"list", "__value__":lsitvalue}
{"__type__":"map", "__value__":mapvalue}

在具体的实现中, 由于字符串会占用较大的字节码空间或影响性能,对于全局结构,可以静态映射为整数表示。

3

符号表及重定位

为实现动态类型, 符号表需要保存在运行时环境,即全局运行时对象环境。对于加减乘除等运算,使用对象类型结合运算符名的修饰方式可确定函数对象;而对于对象的其他成员函数,使用对象名结合成员函数名修饰方式。而重定位的时机是编译完成时,所有的函数偏移已确定。在系统构建好全局对象后,立即跳转到重定位函数去处理需要重定位的符号信息。当需要访问对象时,可以正确获取对象的偏移,如函数调用为伪代码:

function args # 可支持动态参数的参数栈结构
global object # 获取全局运行时对象
push function object index # 将函数对象编码压栈
pickitem     # 获取函数对象。
pick function object offset # 获取函数偏移
DCALL # 跳转

4

全局对象的静态映射

由于直接使用符号索引,会导致字节码增大,且 ARRAY 字节码的处理性能相对 map 更高,所以编译时尽量减少符号的压栈,而使用静态符号表的方式,将全局或局部变量,映射为 index,减少字节码的生成,提高性能。同时,在编译时检查出更多的语法错误,如未定义,重复定义等。全局对象可保存在 array 结构中:

[funcobj, classobj, intobj,  stringobj....]

5

成员对象访问及对象继承处理

如上所述,全局对象保存在全局运行时环境中,而局部对象保存在函数的局部运行时环境中,某个对象的成员变量在访问之前,该对象已从运行时环境中取出。所以,在当访问成员变量时,根据索引成员变量的 key 即可获取。由于是动态类型,无法在编译时根据信息映射为 index 整数,只能直接使用变量名。伪码如下:

push class object
push member object  # 编译器根据成员对象名,生成指令
dup                  # 复制一份作为临时变量
has_Key object        # 判断是否存在
jmpifnot label0     # 如果不存在跳转到label0  
get inherit object  # 获取继承对象,如果不存在继承继承,则运行时报错
swap
lebal0:
Pickitem            # 如果是函数访问,则需要生成DCALL指令

6

运算符实现及重载

由于对象模型的变换,所有的运算符逻辑不能直接使用 NeoVM 的指令逻辑,需要用对应对象的逻辑实现。每个运算符的语义和特定的对象绑定。编译时通过ast获取运算符。对于不同的对象,编译时生成不同的对象运算符函数;运行时根据对象类型的不同跳转到相应的对象处理函数。比如 string 对象的加法和int对象加法,是两个不同的函数实现。

所以根据以上方法,任何对象,都可以重载 add 函数,实现对象的新的加法语义定义。其他运算类似。对于系统内建类型,如 Int、string、list、map。都需要在编译时生成内建的运算符处理函数。

7

控制逻辑

控制逻辑与对象语义关系不大,但是控制指令在判断时需要将对象转换成 Ontology NeoVM 的 Boolean 或 big.int。

8

NeoVM Service 处理

NeoVM service 返回的数据都是 Ontology NeoVM 语义上的, 所以需要根据返回类型的不同,构造为当前设计的对象类型。对 Syscall 的翻译,不能直接使用 Syscall + servicename 的方式。后面还需要加上对应的对象类型构造。而对于 syscall 传入参数是,也需要复原成 Ontology NeoVM 底层语义的对象。

9

结论

由于语言语义的多样性,仅仅直接复用 Ontology NeoVM 原生语义,是不能很好的实现支持语言原生语义的。对象模型的设计,可以使得智能合约支持的语言语义更加精确,扩展能力更强,通过优化不断地接近原生语义,对现有的内建对象 int、 string、list、map 支持更丰富精确的原生语义,对开发者更友好。但是,这同时会产生数倍于当前编译器生成的字节码,而且编译器的实现更加复杂。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 本体研究院 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档