🐨9 库和接口

[TOC]

库和接口

库 library

库与合约类似,库只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL 特性进行重用。

  • 库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别是可以访问调用合约的存储。

  • 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。

  • 因为我们假定库是无状态的,所以如果它们不修改状态(也就是说,如果它们是 view 或者 pure 函数),库函数仅可以通过直接调用来使用(不使用 DELEGATECALL)。

  • 除非能规避 Solidity 的类型系统,否则是不可能销毁任何库的。

  • 库可以看作是使用它们的合约的隐式父类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的父类合约十分类似。

  • 需要使用内部调用约定来调用内部函数,这意味着所有内部类型,内存类型都是通过引用而不是复制来传递。 为了在 EVM 中实现这些,内部库函数的代码和从其中调用的所有函数都在编译阶段被包含到调用合约中,然后使用一个 JUMP 调用来代替 DELEGATECALL。

  • 与合约相比,库没有状态变量、不能够继承或被继承、不能接收以太币、不可以被销毁。

库的实现与使用

这里部署需要支付两次gas,一次是库,一次合约,另外每次调用 register 函数存入数据都要支付 gas 费,因为 register 是外部函数调用。

  • 在库中使用内存类型和内部函数来实现自定义类型,就无需支付外部函数调用的开销。

库的函数签名与选择器

尽管可以对 public 或 external 的库函数进行外部调用,但此类调用会被视为Solidity的内部调用,与常规的 contract ABI 规则不同。

  • 外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。

  • 因此,计算用于计算4字节选择器的函数签名遵循内部命名模式以及可对合约ABI中不支持的类型的参数使用内部编码。

函数签名中存储类型标识符:

  • 值类型, 非存储的 non-storage string 及非存储的 bytes 使用和合约 ABI 中同样的标识符。

  • 非存储 non-storage 的数组类型遵循合约 ABI 中同样的规则,例如 <type>[] 为动态数组, <type>[长度] 固定长度数组。

  • 非存储的结构体使用完整的命名引用,例如 C.S 用于 contract C { struct S { ... } }

  • 存储的映射引用使用 mapping(<keyType> => <valueType>) storage<keyType><valueType> 是映射的键和值类型。

  • 其它的存储的引用类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及 storage

  • 除了指向存储 storage 的引用以外,参数编码与常规合约ABI相同,存储引用被编码为uint256值,指向它们所指向的存储插槽。

  • 与合约 ABI 相似,选择器由签名的 Keccak256 哈希的前四个字节组成。可以使用 .selector 成员从Solidity中获取其值,

库的调用保护

库的代码通过 CALL 来执行,不是用 DELEGATECALLCALLCODE, 那么执行的结果会被回退, 除非是对 view 或者 pure 函数的调用。

  • EVM 没有为合约提供检测是否使用 CALL 的调用方式,但是合约可以使用 address 操作码找出正在运行的 “位置”,生成的代码通过比较这个地址和构造时的地址来确定调用模式。

  • 库的运行时代码是从一个 push 指令开始,它在编译时是 20 字节的零。当运行部署代码时,这个常数被内存中的当前地址替换,修改后的代码存储在合约中。

  • 在运行时,部署时地址就成为了第一个被 push 到堆栈上的常数,对于任何不是 viewpure 函数,调度器代码都将对比当前地址与这个常数是否一致。

  • 库在链上存储的实际代码与编译器输出的 deployedBytecode 的编码是不同的。

using for 便利的库加载语法

在当前合约里, 指令 using A for B; 可用于附加库函数,从库 A 到任何类型 B。 这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self 变量)。

  • using A for *; 的效果是,库 A 中的函数被附加在任意的类型上,所有函数都会被附加一个参数,即使它们的第一个参数类型与对象的类型不匹配,函数调用和重载解析时才会做类型检查。

  • using A for B; 指令仅在当前作用域有效,目前仅限于在当前合约中,后续可能提升到全局范围。 通过引入一个模块,不需要再添加代码就可以使用包括库函数在内的数据类型。

  • 所有 external 库调用都是实际的 EVM 函数调用,如果传递内存或值类型都将产生一个副本,即使是 self 变量。 引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。

接口 interface

接口比抽象合约更加抽象,因此接口的限制要比抽象合约更多。

  • 接口无法继承其它合约,但是可以继承其他接口。

  • 接口中所有的函数都需要是 external 可见性。

  • 接口不能定义构造函数。

  • 接口不能定义状态变量。

  • 接口基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。

  • 接口中的函数都会隐式的标记为 virtual ,意味着他们会被重写。

  • 合约通过 is 继承接口,继承接口就必须实现接口定义的所有函数。

Last updated