库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL
(Homestead之前是用CALLCODE
)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this
将会指向到调用合约,而且可以访问调用合约的storage。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量,否则除此之外,没有任何方法去知道这些状态变量。如果库函数不修改状态(即view
或pure
函数),则只能直接调用库函数(即不使用DELEGATECALL
),因为库被假定为无状态(stateless)的。特别是,除非Solidity的类型系统被规避,否则不可能destroy 库。
使用库的合约,可以将库视为隐式的父合约(base contracts),当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库L
有函数f()
,使用L.f()
即可访问。此外,internal
的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal的调用惯例,这意味着所有internal类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点,internal的库函数的代码和从其中调用的所有函数将被拉取(pull into)到调用合约中,然后执行一个普通的JUMP
来代替DELEGATECALL
。
下面的例子展示了如何使用库(后续在using for章节有一个更适合的实现Set的例子)。
pragma solidity ^0.4.16;
library Set {
// 我们定义了一个新的结构体数据类型,用于存放调用合约中的数据
struct Data { mapping(uint => bool) flags; }
// 注意第一个参数是 “存储引用”类型,这样仅仅是它的地址,
// 而不是它的内容在调用中被传入 这是库函数的特点,
// 若第一个参数用"self"调用时很笨的的,如果这个函数可以
// 被对象的方法可见。
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // 已经在那里
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // 不在那里
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) public {
// 这个库函数没有特定的函数实例被调用,
// 因为“instance”是当前的合约
require(Set.insert(knownValues, value));
}
// 在这个合约里,如果我们要的话,
// 也可以直接访问 knownValues.flags
}
当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用storage类型的参数,还可以在任何位置有多个storage的引用类型的参数。
调用Set.contains
,Set.remove
,Set.insert
都会编译为以DELEGATECALL
的方式调用external的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管msg.sender
,msg.value
,this
还会保持它们在此调用中的值(在Homestead之前,由于实际使用的是CALLCODE
,msg.sender
,msg.value
会变化)。
下面的例子演示了如何使用memory类型和内部函数(inernal function),来实现一个自定义类型,但不会用到外部函数调用(external function)。
pragma solidity ^0.4.16;
library BigInt {
struct bigint {
uint[] limbs;
}
function fromUint(uint x) internal pure returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a, bigint _b) internal pure returns (bigint 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 a = limb(_a, i);
uint b = limb(_b, i);
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == uint(-1) && 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);
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint _a, uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for BigInt.bigint;
function f() public pure {
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}
因为编译器并不知道库最终部署的地址。这些地址须由linker填进最终的字节码中(使用命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符_Set___
(其中Set
是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。
对比普通合约来说,库的限制:
这些限制将来也可能被解除。
正如在引言中提到的,如果库的代码是使用CALL
而不是DELEGATECALL
或CALLCODE
来执行的,那么除非调用一个view
或pure
函数,否则它将恢复。
EVM没有提供一种直接的方法来检测是否使用CALL
来调用它,但是合约可以使用ADDRESS
操作码来查找它当前运行的“何处”。生成的代码将此地址与创建时使用的地址进行比较,以确定调用模式。
更具体地说,库的运行时代码总是从编译时为20字节的0的推送指令开始。当部署代码运行时,该常数由当前地址在内存中被替换,并且该修改的代码被存储在合约中。在运行时,这会导致部署时间地址是第一个被推到堆栈上的常量,调度器代码将当前地址与此常量对任何非视图和非纯函数进行比较。
上一篇:深入理解Solidity——抽象合约和接口
下一篇:深入理解Solidity——Using for