同大多数编程语言一样,用 Solidity
编写的智能合约无法直接在以太坊虚拟机(EVM
)上运行,必须先将其编译成字节码。编译需要使用 Solidity 编译器。
Solidity 编译器是 c++
语言编写的,因此对于操作系统而言,可以很轻易的编译成二进制命令行程序。而对于浏览器环境则需要使用工具 Emscripten 将 c++
代码编译成 js
代码。
相关仓库:
- Solidity 编译器
- Solidity 编译器 各版本被编译在不同平台上的文件列表 solc-bin
nodejs
版本的编译器 solc-js,是对被编译成的js
代码二次封装, 新增了命令行, 支持模块化等。智能合约开发框架hardhat
正是用的该框架进行智能合约的编译。
具体的编译过程则是将指定结构的 JSON
数据输入到编译器中, 编译完成后输出编译结果。
在开始之前需要先介绍下抽象语法树(AST)。
抽象语法树
由 Solidity
转换为字节码,可以看成是由一种语言转换成另一种语言的过程。因此需要一种中间介质来承载原语言解析的结果,再将中间介质转换为目标语言,这种介质就是抽象语法树。也是进行语法转换的基础。如果你是一名 js
开发者,相信对这个概念并不陌生。
抽象语法树的构建:
- 词法分析:将源代码分解成一系列的标记(
token
),token
是语言中的基本语法单位,例如关键字、标识符、运算符和标点符号等。 - 语法分析:语法分析器会根据
Solidity
的语法规则,将这些token
组合成语法结构,如表达式、声明和语句等。 - 构建抽象语法树:在语法分析的基础上,编译器构建一个树形结构,即抽象语法树。每个节点代表一个语言构造,例如一个函数定义、一个变量声明或一个运算表达式。这棵树反映了源代码的层级和结构关系。
- 语义分析:虽然不是直接构建 AST 的一部分,但语义分析是确保 AST 正确性的重要步骤。在这个阶段,编译器会检查类型一致性、变量和函数的作用域、以及其他语义规则。
- 优化:在某些情况下,AST 在生成最终的机器代码或字节码之前会经过优化,以提高效率和性能。
例如对于智能合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
contract Main { function hack() public { } }
转换成的抽象语法树为
const ast = {
absolutePath: 'main',
exportedSymbols: {
Main: [6]
},
id: 7,
license: 'GPL-3.0',
nodeType: 'SourceUnit',
nodes: [
{
id: 1,
literals: ['solidity', '^', '0.8', '.21'],
nodeType: 'PragmaDirective',
src: '42:24:0'
},
{
abstract: false,
baseContracts: [],
canonicalName: 'Main',
contractDependencies: [],
contractKind: 'contract',
fullyImplemented: true,
id: 6,
linearizedBaseContracts: [6],
name: 'Main',
nameLocation: '82:4:0',
nodeType: 'ContractDefinition',
nodes: [
{
body: {
id: 4,
nodeType: 'Block',
src: '112:3:0',
statements: []
},
functionSelector: '4de260a2',
id: 5,
implemented: true,
kind: 'function',
modifiers: [],
name: 'hack',
nameLocation: '98:4:0',
nodeType: 'FunctionDefinition',
parameters: {
id: 2,
nodeType: 'ParameterList',
parameters: [],
src: '102:2:0'
},
returnParameters: {
id: 3,
nodeType: 'ParameterList',
parameters: [],
src: '112:0:0'
},
scope: 6,
src: '89:26:0',
stateMutability: 'nonpayable',
virtual: false,
visibility: 'public'
}
],
scope: 7,
src: '73:44:0',
usedErrors: [],
usedEvents: []
}
],
src: '42:75:0'
}
编译输入
Solidity
编译是将指定结构的 JSON
数据输入到编译器中, 编译完成后输出编译结果。
编译输入包括下列字段
数据类型见源码中 parseInput 方法
language
使用的代码语言, 支持 Solidity
、Yul
、SolidityAST
Yul
是一种底层语言,它的抽象级别更低,更接近以太坊虚拟机的操作。可以在 Solidity
中通过内 联汇编的方式直接编写 Yul
代码。同时 Yul
存在优化器,可以执行诸如简化表达式、消除未被使用的代码以及合并相同代码等操作,以减小生成的字节码大小和运行时的 Gas 消耗。
SolidityAST
则是直接输入 AST 到编译器中, 省略了编译成 AST 的过程。
sources
代码字符串, 结构为
sources: {
'main.sol': {
keccak256: '0x...',
content: `// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
contract Main { uint a = 1; function add() public { a += 1; } }`
urls: [
"bzzr://...",
"ipfs://...",
"/path"
]
},
'file-key': {
...
},
}
sources
字段提供需要编译的文件 key
和文件内容的映射,文件 key
通常设置为文件名。文件内容有以下字段
keccak256
, 可选,源码内容的keccak256
哈希值content
: 需要编译的源码内容字符串urls
: 需要编译的源码内容链接,content
和urls
只需要存在一个即可。同时存在时则只读取content
, 忽略urls