剖析六种 EVM 语言:优秀的语言要如何设计

WB3交流加微信:WX-93588,⬅️此处为全站广告位,与正文项目无关
注册并登录App即可领取高达 60,000 元的数字货币盲盒:点击此处注册OKX

以太坊虚拟机 (EVM) 是一个 256 位、根据堆栈、全球可拜访的图灵机。因为架构与其他虚拟机和物理机的明显不同,EVM 需求领域特定言语 DSL(注:领域特定言语指的是专注于某个应用程序领域的核算机言语)。

在本文中,咱们将研讨 EVM DSL 规划的最新技术,介绍六种言语 Solidity、Vyper、Fe、Huff、Yul 和 ETK。

言语版别

  • Solidity: 0.8.19

  • Vyper: 0.3.7

  • Fe: 0.21.0

  • Huff: 0.3.1

  • ETK: 0.2.1

  • Yul: 0.8.19

阅读本文,需求你对 EVM、堆栈和编程有根本的了解。

以太坊虚拟机概述

EVM 是一个根据 256 位堆栈的图灵机。可是,在深入研讨它的编译器之前,应该介绍一些功用特性。

因为 EVM 是「图灵完备」的,它会遭到「停机问题」的困扰。简而言之,在程序履行之前,没有办法确定它未来是否会停止。EVM 处理这个问题的办法是经过「Gas」计量核算单位,一般来说,这与履行指令所需的物理资源成份额。每个买卖的 Gas 量是有束缚的,买卖的发起者有必要支付与买卖耗费的 Gas 成份额的 ETH。这个策略的影响之一是,假如有两个功用上相同的智能合约,耗费更少 Gas 的合约将被更多选用。这导致协议竞争极点的 Gas 功率,工程师努力最小化特定任务的 Gas 耗费。

此外,当调用一个合约时,它会创立一个履行上下文。在这个上下文中,合约有一个堆栈用于操作和处理,一个线性内存实例用于读写,一个本地耐久性存储用于合约读写,并且附加到调用的数据「calldata」能够被读取但不能被写入。

关于内存的一个重要阐明是,尽管它的巨细没有确定的「上限」,但仍然是有限的。扩展内存的 Gas 本钱是动态:一旦达到阈值,扩展内存的本钱将呈二次方增长,也就是说 Gas 本钱与额定内存分配的平方成正比。

合约也能够运用一些不同的指令来调用其他合约。 「call」指令将数据和可选的 ETH 发送到方针合约,然后创立自己的履行上下文,直到方针合约的履行停止。 「staticcall」指令与 「call」相同,但增加了一个查看,即在静态调用完结之前,断言全局状况的任何部分都未被更新。终究, 「delegatecall」指令的行为相似于 「call」,仅仅它会保存从前上下文的一些环境信息。这一般用于外部库和代理合约。

为什么言语规划很重要

在与非典型架构交互时,特定领域言语(DSL)是必要的。尽管存在诸如 LLVM 之类的编译器东西链,可是依赖它们来处理智能合约,在程序正确性和核算功率至关重要的状况下,不太抱负。

程序正确性十分重要,因为智能合约默许是不行变的,并且鉴于区块链虚拟机(VM)的特点,智能合约是金融应用程序的抢手挑选。尽管存在针对 EVM 的升级性处理方案,但它充其量仅仅一个补丁,最坏的状况是恣意代码履行漏洞。

核算功率也十分要害,因为最小化核算具有经济优势,但不能以安全为价值。

简而言之,EVM DSL 有必要平衡程序正确性和 Gas 功率,在不牺牲太多灵活性的状况下经过做出不同的取舍来完结其中之一。

言语概览

关于每种言语,咱们将描述它们的明显特性和规划挑选,并包括一个简略的计数功用智能合约。言语盛行度是根据 Defi Llama 上的总确定价值 (TVL) 数据确定的。

Solidity

Solidity 是一种高档言语,其语法相似于 C、Java 和 Javascript。它是按 TVL 核算最受欢迎的言语,其 TVL 是第二名的十倍。为了代码重用,它运用面向目标办法,智能合约被视为类目标,利用了多重承继。编译器选用 C++ 编写,方案在将来迁移到 Rust。

可变的合约字段存储在耐久性存储中,除非它们的值在编译时(常量)或布置时(不行变)已知。合约内声明的办法能够声明为 pure、view、payable,或默许状况下是 non-payable 但状况可修正。pure 办法不会从履行环境中读取数据,也不能读取或写入耐久性存储;也就是说,给定相同的输入,pure 办法将始终回来相同的输出,它们不会产生副效果。view 办法能够从耐久性存储或履行环境中读取数据,但它们不能写入耐久性存储,也不能创立副效果,例如附加业务日志。payable 办法能够读写耐久性存储,从履行环境中读取数据,产生副效果,并且能够接收附加在调用中的 ETH。non-payable 办法与 payable 办法相同,但具有运行时查看,以断言当时履行上下文中没有附加 ETH。

在合约的规模内声明时,办法能够指定以下四种可见性修饰符:private、internal、public 或 external。private 办法能够经过当时合约内的「jump」指令在内部拜访。任何承继的合约都不能直接拜访 private 办法。internal 办法也能够经过「jump」指令在内部拜访,但承继的合约能够直接运用内部办法。public 办法能够经过「call」指令由外部合约拜访,创立一个新的履行上下文,并在直接调用办法时经过跳转进行内部拜访。public 办法也能够经过在办法调用前加上「this.」来在新的履行上下文中从同一合约中拜访。external 办法只能经过「call」指令拜访,无论是来自不同的合约还是在同一合约内,都需求在办法调用前加上「this.」。

Solidity 还供给了三种界说库的办法。第一种是外部库,它是一个无状况的合约,独自布置到链上,在调用合约时动态链接,并经过「delegatecall」指令拜访。这是最不常见的办法,因为外部库的东西支持缺乏,「delegatecall」很贵重,它有必要从耐久存储中加载额定的代码,并且需求多个业务进行布置。内部库的界说办法与外部库相同,仅仅每个办法有必要界说为内部办法。在编译时,内部库被嵌入到终究合约中,并且在死代码分析阶段,库中未运用的办法将被删除。第三种办法与内部库相似,但不是在库内界说数据结构和功用,而是在文件等级界说,并且能够直接导入和在终究合约中运用。第三种办法供给了更好的人机交互性,能够运用自界说数据结构,将函数应用于全局效果域中,并必定限程度上将别号运算符应用于某些函数。

编译器供给两个优化通道。第一个是指令级优化器,对终究的字节码履行优化操作。第二个是近期增加运用 Yul 言语(稍后具体介绍)作为编译过程中的中心表明(IR),然后对生成的 Yul 代码进行优化操作。

为了与合约中的公共和外部办法交互,Solidity 规定了一种应用程序二进制接口(ABI)标准来与其合约交互。现在,Solidity ABI 被视为 EVM DSL 的事实标准。指定外部接口的以太坊 ERC 标准都依照 Solidity 的 ABI 标准和风格指南来履行。其他言语也遵从 Solidity 的 ABI 标准,很少出现偏差。

Solidity 还供给了内联 Yul 块,答应对 EVM 指令集进行低等级拜访。Yul 块包括 Yul 功用的子集,具体信息请参见 Yul 部分。这一般用于进行 Gas 优化,利用高档语法不支持的功用,并自界说存储、内存和 calldata。

因为 Solidity 的盛行,开发人员东西十分成熟且规划精巧,Foundry 是在这方面突出的代表。

以下是用 Solidity 编写的一个简略合约:

Vyper

Vyper 是一种语法相似于 Python 的高档言语。它几乎是 Python 的一个子集,只要一些小的不同。它是第二受欢迎的 EVM DSL。Vyper 针对安全性、可读性、审计才干和 Gas 功率进行了优化。它不选用面向目标办法、内联汇编,并且不支持代码重用。它的编译器是用 Python 编写的。

存储在耐久性存储器中的变量是在文件等级声明的。假如它们的值在编译时已知,能够将它们声明为「constant(常量)」;假如它们的值在布置时已知,则能够将它们声明为「immutable(不变量)」;假如它们被标记为 public,则终究合约将为该变量揭露一个只读函数。常量和不变量的值经过它们的称号在内部拜访,可是耐久性存储器中的可变量能够经过在称号前面增加「self.」来拜访。这关于避免存储变量、函数参数和局部变量之间的命名空间冲突十分有用。

和 Solidity 相似,Vyper 也运用函数特点来表明函数的可见性和可变性。被标记为「@external」的函数能够经过「call」指令从外部合约拜访。被标记为「@internal」的函数只能在同一合约中拜访,并且有必要以「self.」为前缀。被标记为「@pure」的函数不能从履行环境或耐久存储中读取数据,也不能写入耐久存储或创立任何副效果。被标记为「@view」的函数能够从履行环境或耐久存储中读取数据,但不能写入耐久存储或创立副效果。被标记为「@payable」的函数能够读取或写入耐久存储,创立副效果,承受收 ETH。没有声明这个可变性特点的函数默许为 non-payable,也就是说,它们和 payable 函数一样,但不能接收 ETH。

Vyper 编译器还挑选将局部变量存储在内存中而不是堆栈上。这使得合约愈加简略和高效,并处理了其他高档言语中常见的「堆栈过深」的问题。可是,这也带来了一些折衷。

另外,因为内存布局有必要在编译时知道,因而动态类型的最大容量也有必要在编译时知道,这是一个束缚。此外,分配很多内存会导致非线性的 Gas 耗费,正如 EVM 概述部分中提到的。可是,关于许多用例来说,这个 Gas 本钱能够忽略不计。

尽管 Vyper 不支持内联汇编,但它供给了更多内置函数,以确保几乎每个 Solidity 和 Yul 中的功用在 Vyper 中也能够完结。经过内置函数能够拜访初级位运算、外部调用和代理合约操作,经过编译时供给掩盖文件能够完结自界说存储布局。

Vyper 没有丰厚的的开发东西套件,但它有更紧密集成的东西,并且也能够插入到 Solidity 开发东西中。值得重视的 Vyper 东西包括 Titanaboa 解说器,它具有许多与 EVM 和 Vyper 相关的内置东西,可用于实验和开发,以及 Dasy,一种根据 Vyper 的 Lisp,具有编译时代码履行功用。

下面是用 Vyper 编写的一个简略合约:

Fe

Fe 是一种相似 Rust 的高档言语,现在正在积极开发中,大部分功用尚未推出。它的编译器主要用 Rust 编写,但运用 Yul 作为其中心表明办法(IR),依赖于用 C++ 编写的 Yul 优化器。跟着 Rust 原生后端 Sonatina 的参加,这一点有望改变。Fe 运用模块进行代码同享,因而不运用面向目标的办法,而是经过根据模块的体系重用代码,在模块内声明变量、类型和函数,能够以相似于 Rust 的办法进行导入。

耐久存储变量在合约等级声明,假如没有手动界说的 getter 函数则不行揭露拜访。常量能够在文件或模块等级声明,并且能够在合约内部拜访。当时不支持不行变的布置时变量。

办法能够在模块等级或合约内声明,默许是 pure 和 private。要使合约办法揭露,有必要在界说前加上「pub」要害字,这使得它能够在外部拜访。要从耐久化存储变量中读取,办法的第一个参数有必要是「self」,在变量名前加上「self.」,使该办法具有只读拜访本地存储变量的权限。要读取和写入耐久化存储,第一个参数有必要是「mut self」。「mut」要害字表明合约的存储在办法履行期间是可变的。拜访环境变量是经过将「Context」参数传递给办法来完结的,一般命名为「ctx」。

函数和自界说类型能够在模块等级声明。默许状况下,模块项都是私有的,除非加上「pub」要害字才干拜访。可是,不要和合约等级的「pub」要害字混杂。模块的公共成员只能在终究合约或其他模块内部拜访。

Fe 暂时不支持内联汇编,相反,指令由编译器内部函数或在编译时解析为指令的特别函数包装。

Fe 遵从 Rust 的语法和类型体系,支持类型别号、带有子类型的枚举、特征和泛型。现在这方面的支持还有限,但正在进行中。特征能够针对不同类型进行界说和完结,但不支持泛型,也不支持特征束缚。枚举支持子类型,并能够在其上完结办法,但不能在外部函数中对其进行编码。尽管 Fe 的类型体系仍在开展中,但它在为开发人员编写更安全、编译时查看的代码方面显示出了很大的潜力。

下面是用 Fe 编写的一个简略的合约:

Huff

Huff 是一种汇编言语,具有手动堆栈操控和对 EVM 指令集的最小化笼统。经过「#include」指令,编译时能够解析任何包括的 Huff 文件,然后完结代码重用。开始由 Aztec 团队编写用于极度优化的椭圆曲线算法,编译器后来被用 TypeScript 重写,然后又被用 Rust 重写。

常量有必要在编译时界说,现在不支持不行变量,并且言语中没有显式界说耐久性存储变量。因为命名存储变量是高档笼统,因而在 Huff 中写入耐久性存储是经过操作码 「sstore」 写入和 「sload」读取。自界说存储布局能够由用户界说,也能够依照常规从零开始并且每个变量递加运用编译器内涵的「FREE_STORAGE_POINTER」。使存储变量外部可拜访需求手动界说一个能够读取并回来变量给调用者的代码途径。

外部函数也是高档言语引入的笼统,因而在 Huff 中没有外部函数的概念。可是,大多数项目在不同程度上遵从其他高档言语的 ABI 标准,最常见的是 Solidity。一个常见的办法是界说一个「调度程序」,加载原始调用数据并运用它来查看是否匹配函数挑选器。假如匹配,则履行其后续代码。因为调度程序是用户界说的,因而它们或许遵从不同的调度办法。Solidity 按称号字母次序对其调度程序中的挑选器进行排序,Vyper 按数字次序排序并在运行时履行二进制查找,大多数 Huff 调度程序按预期的函数运用频率排序,很少运用跳转表。现在,跳转表在 EVM 中不被原生支持,因而需求运用相似「codecopy」的内省指令才干完结。

内部函数运用「#define fn」指令界说,能够承受模板参数以进步灵活性,并指定函数开始和结束时的预期堆栈深度。因为这些函数是内部的,因而无法从外部拜访,在内部拜访需求运用「jump」指令。

其他操控流程,例如条件句子和循环句子能够运用跳转方针界说。跳转方针是由标识符后跟冒号界说的。能够经过将标识符压入堆栈并履行跳转指令来跳转到这些方针。这在编译时解析为字节码偏移量。

宏由「#define macro」界说,其他方面与内部函数相同。要害差异在于宏不会在编译时生成「jump」指令,而是将宏的主体直接复制到文件中的每个调用中。

这种规划权衡了减少恣意跳转与运行时 Gas 本钱之间的联系,价值是调用屡次时代码的巨细增加。「MAIN」 宏被视为合约的进口,并且其主体中的第一条指令将成为运行时字节码中的第一条指令。

编译器内置的其他特性还包括为日志记载生成事情哈希、为调度生成函数挑选器、为过错处理生成过错挑选器以及内部函数和宏的代码巨细查看器等。

下面是用 Huff 编写的一个简略合约:

ETK

EVM 东西包(ETK)是一种具有手动堆栈管理和最小化笼统的汇编言语。代码能够经过「%include」和「%import」指令进行重用,编译器是用 Rust 编写的。

Huff 和 ETK 之间的一个明显差异是,Huff 为 initcode 增加了细微的笼统,也称为结构函数代码,这些代码能够经过界说特别的「CONSTRUCTOR」宏来掩盖。在 ETK 中,这些不会被笼统化,initcode 和运行时代码有必要一同界说。

与 Huff 相似,ETK 经过「sload」和「sstore」指令读写耐久性存储。可是,没有常量或不行变要害字,可是能够运用 ETK 中的两种宏之一来模拟常量,即表达式宏。表达式宏不会解析为指令,而是生成可用于其他指令中的数字值。例如,它或许不会完全生成「push」指令,但或许会生成一个数字以包括在「push」指令中。

如前所述,外部函数是高档言语概念,因而在外部揭露代码途径需求创立函数挑选器调度程序。

内部函数不像其他言语那样能够显式界说,而是能够为跳转方针指定用户界说的别号,并经过其称号跳转到它们。这也答应其他操控流,例如循环和条件句子。

ETK 支持两种宏。第一种是表达式宏,能够承受恣意数量的参数并回来可用于其他指令的数字值。表达式宏不生成指令,而是生成立即值或常量。可是,指令宏承受恣意数量的参数,并在编译时生成恣意数量的指令。ETK 中的指令宏相似于 Huff 宏。

下面是 ETK 用编写的一个简略合约:

Yul

Yul 是一种具有高档操控流和很多笼统的汇编言语。它是 Solidity 东西链的一部分,并能够挑选在 Solidity 编译通道中运用。 Yul 不支持代码重用,因为它旨在成为编译方针而不是独立言语。它的编译器是用 C++ 编写的,方案将其与 Solidity 通道的其余部分一同迁移到 Rust。

在 Yul 中,代码被分成目标,这些目标能够包括代码、数据和嵌套目标。因而,Yul 中没有常量或外部函数。需求界说函数挑选器调度程序才干将代码途径揭露到外部。

除了堆栈和操控流指令外,大多数指令在 Yul 中都作为函数揭露。指令能够嵌套以缩短代码长度,也能够分配给临时变量,然后传递给其他指令运用。条件分支能够运用「if」块,假如值为非零,则履行该块,但没有「else」块,因而处理多个代码途径需求运用「switch」处理恣意数量的状况和「default」后备选项。循环能够运用「for」循环履行;尽管其语法与其他高档言语不同,但供给了相同的根本功用。能够运用「function」要害字界说内部函数,并且与高档言语的函数界说相似。

Yul 中的大多数功用在 Solidity 中运用内联汇编块揭露。这答应开发人员打破笼统,编写自界说功用或在高档语法中不行用的功用中运用 Yul。可是,运用此功用需求深入了解 Solidity 在 calldata、memory 和 storage 方面的行为。

还有一些独特的函数。 「datasize」,「dataoffset」和「datacopy」函数经过其字符串别号操作 Yul 目标。 「setimmutable」和「loadimmutable」函数答应在结构函数中设置和加载不行变参数,尽管它们的运用遭到束缚。 「memoryguard」函数表明只分配给定的内存规模,然后使编译器能够运用超出维护规模的内存进行附加优化。终究,「verbatim」答应运用 Yul 编译器不知道的指令。

下面是用 Yul 编写的一个简略合约:

优异 EVM DSL 的特性

一个优异的 EVM DSL 应该从这儿列出的每种言语的优缺点中学习,还应该包括几乎一切现代言语中的基础,如条件句子、办法匹配、循环、函数等等。代码应该是清晰的,为了代码美观或可读性而增加最少的隐式笼统。在高风险、正确性至关重要的环境中,每行代码都应该是清晰可解说的。此外,一个界说杰出的模块体系应该是任何巨大言语的中心。它应该清楚地阐明哪些项界说在哪个效果域中,以及哪些能够拜访。默许状况下,模块中的每个项都应该是私有的,只要显式公共项才干在外部揭露拜访。

在 EVM 这样的资源受限环境中,功率很重要。功率一般经过供给低本钱的笼统来完结,如经过宏进行编译时代码履行,丰厚的类型体系来创立规划杰出的可重用库以及常见的链上交互包装器。宏在编译时生成代码,这关于减少常见操作的样板代码十分有用,在像 Huff 这样的状况下,它可用于在代码巨细与运行时功率之间进行权衡。丰厚的类型体系答应更具表现力的代码、更多的编译时查看以在运行时之前捕获过错,并且当与类型查看的编译器内部函数结合运用时,或许会消除大部分内联汇编的需求。泛型还答应可空值(例如外部代码)被包装在「选项」类型中,或许易出错的操作(例如外部调用)被包装在「成果」类型中。这两种类型是库编写者怎么经过界说代码途径或恢复失败成果的业务来强制开发人员处理每个成果的示例。可是,请记住,这些是编译时笼统,会在运行时解析为简略的条件跳转。强制开发人员在编译时处理每个成果会增加初始开发时间,但好处是运行时的意外状况要少得多。

灵活性关于开发人员也很重要,因而,尽管杂乱操作的默许状况应该是安全且或许不那么高效的道路,但有时需求运用更高效的代码途径或不支持的功用。为此,应该向开发人员敞开内联汇编,并且没有护栏。Solidity 的内联汇编为了简略和更好的优化器传递设置了一些护栏,可是当开发人员需求完全操控履行环境时,他们应该被颁发这些权利。

一些或许有用的功用包括能够在编译时操作函数和其他项的特点。例如,「inline」特点能够将简略函数的主体复制到每个调用中,而不是为了功率创立更多的跳转。而「abi」特点能够答应手动掩盖给定外部函数生成的 ABI,以习惯不同代码风格的言语。此外,还能够界说一个可选的函数调度器,答应在高档言语内进行定制,以便对预期更常用的代码途径进行额定的优化。例如,在履行「name」之前查看挑选器是否为「transfer」或「transferFrom」。

定论

EVM DSL 规划任重而道远。每种言语都有自己独特的规划决策,我期待看到它们在未来怎么开展。作为开发人员,学习尽或许多的言语符合咱们的最大利益。首先,学习多种言语并了解它们的不同之处和相似之处将加深咱们对编程和底层机器体系结构的理解。其次,言语具有深远的网络效应和强大的保存特性。毫无疑问,大型参与者都在构建自己的编程言语,从 C#、Swift 和 Kotlin 到 Solidity、Sway 和 Cairo。学习在这些言语之间无缝切换为软件工程职业供给了无与伦比的灵活性。终究,重要的是要了解每一种言语背后都需求付出很多的工作。没有人是完美的,但很多有才华的人付出了很多努力,为像咱们这样的开发者发明安全愉快的体会。

来历:DeFi之道

版权声明:本文收集于互联网,如有侵权请联系站长删除。
转载请注明:剖析六种 EVM 语言:优秀的语言要如何设计 | 币百度

相关文章