库
库类似于合约,但它们的目的是它们只在特定地址部署一次,并且使用 EVM 的DELEGATECALL (CALLCODE直到 Homestead)功能重用它们的代码。这意味着如果调用库函数,它们的代码将在调用合约的上下文中执行,即this指向调用合约,尤其是可以访问调用合约的存储。由于库是一段孤立的源代码,它只能访问调用合约的状态变量,如果它们被显式提供(否则它无法命名它们)。如果库函数DELEGATECALL不修改状态(即如果它们是view或pure函数),因为库被假定为无状态的。特别是,不可能销毁库。
笔记
在 0.4.20 版本之前,可以通过绕过 Solidity 的类型系统来销毁库。从那个版本开始,库包含一种不允许直接调用状态修改函数的机制DELEGATECALL(即没有)。
库可以被视为使用它们的合约的隐含基础合约。它们在继承层次结构中不会显式可见,但对库函数的调用看起来就像对显式基础合约的函数的调用(使用限定访问,如L.f())。当然,对内部函数的调用使用内部调用约定,这意味着所有内部类型都可以传递,并且存储在内存中的类型将通过引用传递而不是复制。为了在 EVM 中实现这一点,从合约调用的内部库函数的代码以及从其中调用的所有函数将在编译时包含在调用合约中,并且JUMP将使用常规调用而不是DELEGATECALL.
笔记
当涉及到公共函数时,继承类比就失效了。调用公共库函数L.f()会导致外部调用(DELEGATECALL 准确地说)。相反,当是当前合约的基础合约时,A.f()是内部调用。A
以下示例说明了如何使用库(但使用手动方法,请务必查看using for以获得更高级的示例来实现集合)。
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; // We define a new struct datatype that will be used to // hold its data in the calling contract. struct Data { mapping(uint => bool) flags; } library Set { // Note that the first parameter is of type "storage // reference" and thus only its storage address and not // its contents is passed as part of the call. This is a // special feature of library functions. It is idiomatic // to call the first parameter `self`, if the function can // be seen as a method of that object. function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // already there self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; // not there self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; } } contract C { Data knownValues; function register(uint value) public { // The library functions can be called without a // specific instance of the library, since the // "instance" will be the current contract. require(Set.insert(knownValues, value)); } // In this contract, we can also directly access knownValues.flags, if we want. }
当然,您不必按照这种方式使用库:也可以在不定义结构数据类型的情况下使用它们。函数也可以在没有任何存储引用参数的情况下工作,并且它们可以有多个存储引用参数并且可以在任何位置。
对和的调用Set.contains都 编译为对外部合约/库的调用 ( )。如果您使用库,请注意执行了实际的外部函数调用。 ,并且将在此调用中保留它们的值(在 Homestead 之前,因为使用,并且 更改了)。Set.insertSet.removeDELEGATECALLmsg.sendermsg.valuethisCALLCODEmsg.sendermsg.value
以下示例显示了如何使用存储在内存中的类型和库中的内部函数来实现自定义类型,而无需外部函数调用的开销:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; struct bigint { uint[] limbs; } library BigInt { function fromUint(uint x) internal pure returns (bigint memory r) { r.limbs = new uint[](1); r.limbs[0] = x; } function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) { r.limbs = new uint[](max(a.limbs.length, b.limbs.length)); uint carry = 0; for (uint i = 0; i < r.limbs.length; ++i) { uint limbA = limb(a, i); uint limbB = limb(b, i); unchecked { r.limbs[i] = limbA + limbB + carry; if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0)) carry = 1; else carry = 0; } } if (carry > 0) { // too bad, we have to add a limb uint[] memory newLimbs = new uint[](r.limbs.length + 1); uint i; for (i = 0; i < r.limbs.length; ++i) newLimbs[i] = r.limbs[i]; newLimbs[i] = carry; r.limbs = newLimbs; } } function limb(bigint memory a, uint index) internal pure returns (uint) { return index < a.limbs.length ? a.limbs[index] : 0; } function max(uint a, uint b) private pure returns (uint) { return a > b ? a : b; } } contract C { using BigInt for bigint; function f() public pure { bigint memory x = BigInt.fromUint(7); bigint memory y = BigInt.fromUint(type(uint).max); bigint memory z = x.add(y); assert(z.limb(1) > 0); } }
可以通过将库类型转换为类型来获取库的地址address,即使用address(LibraryName).
由于编译器不知道库将部署到的地址,因此编译后的十六进制代码将包含表单的占位符__$30bbc0abd4d6364515865950d3e0d10953$__。占位符是完全限定库名称的 keccak256 哈希的十六进制编码的 34 个字符前缀,例如libraries/bigint.sol:BigInt,如果库存储在目录中调用的文件bigint.sol中libraries/。此类字节码不完整,不应部署。占位符需要替换为实际地址。您可以通过在编译库时将它们传递给编译器或使用链接器更新已编译的二进制文件来做到这一点。有关如何使用命令行编译器进行链接的信息,请参阅 库链接。
与合约相比,库在以下方面受到限制:
- 他们不能有状态变量
- 他们不能继承也不能被继承
- 他们无法接收以太币
- 他们不能被摧毁
(这些可能会在稍后解除。)
库中的函数签名和选择器
虽然可以对公共或外部库函数进行外部调用,但此类调用的调用约定被认为是 Solidity 内部的,与为常规合约 ABI指定的不同。外部库函数支持比外部合约函数更多的参数类型,例如递归结构和存储指针。出于这个原因,用于计算 4 字节选择器的函数签名是按照内部命名模式计算的,并且合约 ABI 中不支持的类型的参数使用内部编码。
以下标识符用于签名中的类型:
-
值类型、非存储
string
和非存储bytes
使用与合约 ABI 中相同的标识符。 -
非存储数组类型遵循与合同 ABI 中相同的约定,即
<type>[]
用于动态数组和<type>[M]
固定大小的M
元素数组。 -
非存储结构由它们的完全限定名称引用,即
C.S
for 。contract C { struct S { ... } }
-
存储指针映射分别使用where和是映射的键和值类型的标识符。
mapping(<keyType> => <valueType>) storage
<keyType>
<valueType>
-
其他存储指针类型使用其对应的非存储类型的类型标识符,但在其后附加一个空格
storage
。
参数编码与常规合约 ABI 相同,除了存储指针,它们被编码为 uint256
引用它们指向的存储槽的值。
与合约 ABI 类似,选择器由签名的 Keccak256-hash 的前四个字节组成。它的值可以使用.selector
如下成员从 Solidity 获得:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.14 <0.9.0; library L { function f(uint256) external {} } contract C { function g() public pure returns (bytes4) { return L.f.selector; } }
库的呼叫保护
正如介绍中提到的,如果使用 aCALL
而不是DELEGATECALL
or来执行库的代码,除非调用or函数CALLCODE
,否则它将恢复原状。view
pure
EVM 没有为合约提供直接的方法来检测它是否被调用CALL
,但是合约可以使用ADDRESS
操作码来找出它当前运行的“位置”。生成的代码将此地址与构建时使用的地址进行比较,以确定调用方式。
更具体地说,库的运行时代码总是以 push 指令开始,它在编译时是 20 字节的零。当部署代码运行时,这个常量在内存中被当前地址替换,修改后的代码存储在合约中。在运行时,这会导致部署时间地址成为第一个被压入堆栈的常量,并且调度程序代码将当前地址与任何非视图和非纯函数的该常量进行比较。
这意味着存储在链上的库的实际代码与编译器报告的代码不同 deployedBytecode
。