Solidity的库(二十七)|入门系列

2017/6/17 posted in  Solidity入门系列

库与合约类型,但他们的目的是重用代码1。通过EVM中的DELEGATECALL特性来调用部署到某个位置的库代码,就实现了复用。

库函数运行的上下文

DELEGATECALL指令意味着,代码是在发起调用合约的context中被执行的,因此this将指向到发起调用的合约。我们来看看下面的例子:

pragma solidity ^0.4.0;

library DelegateCalledLibary{
  function relatedVar() returns (address, address, uint){
    return (this, msg.sender, msg.value);
  }
}

contract CalledContract{

  function calling() returns(address, address, address, address, uint, uint){
    var (libAddr, libSender, libVal) = DelegateCalledLibary.relatedVar();
    var (contractAddr, contractSender, contractVal) = (this, msg.sender, msg.value);

    return (libAddr, contractAddr, libSender, contractSender, libVal, contractVal);
  }
}

上面的例子中,调用CalledContract中的calling()方法,我们会发现库里的thismsg.sendermsg.value与调用合约中的完全一致。虽然如此,但需要注意的是,对于库的调用,实际会被编译为DELEGATECALL的向外部合约或库的调用,如果使用库函数,实际意味着你正在发起一个external的函数调用(在Homestead版本之前,由于底层是使用的CALLCODE实现的,所以msg.sendermsg.value值会变)。

另外从上面可以看到,库函数使用时,不需要实例化,就可以通过库.函数的方式直接访问。

访问状态变量

在库中,也可调用合约的storage类型的变量,不过需要明确的传入状态变量。这里来看一个例子来说明一下:

pragma solidity ^0.4.0;

library SimpleLib{
  //这里只是定义了一个结构体
  struct Data{
    mapping(uint => bool) flags;
  }
  //访问传进来的状态变量
  function contains(Data storage self, uint val) returns(bool){
    return self.flags[val];
  }
}

contract AccessState{
  SimpleLib.Data data;

  function calling() returns(bool, bool){
    bool contain1 = SimpleLib.contains(data, 1);
    data.flags[2] = true;
    bool contain2 = SimpleLib.contains(data, 2);

    return (contain1, contain2);
  }
}

由于库函数一般是公共的代码,所以如上例,可以在库函数中定义相应的结构体。结构体的实际实例则放在调用的合约中,如上例,在AccessState合约中,定义的SimpleLib.Data data;。通过参数的方式,可以将data再传回库函数中,复用库函数的代码。

上述代码中,需要注意的是,在调用合约代码中,你可以直接操作传入库函数的数据,如上述代码中的data.flags[2] = true;

Using For

我们可以使用指令using A for B来附着库A里定义的函数来任意的类型B。A库里的函数允许一种惯例。这些库函数可以接收调用函数的实例对象作为第一个参数(类似Python中的特殊self变量)。使用这种语法,我们可以让上一个例子的实现更加简单。下面来看改后的代码实现:

pragma solidity ^0.4.0;

library Set{
  //这里只是定义了一个结构体
  struct Data{
    mapping(uint => bool) flags;
  }
  //访问传进来的状态变量
  function contains(Data storage self, uint val) returns(bool){
    return self.flags[val];
  }
}

contract LibraryUsingFor{
  //这是关键的一步
  using Set for Set.Data;
  Set.Data data;

  function call() returns(bool, bool){
    //对应的对象上被附着了库函数,可以直接调用
    bool contain1 = data.contains(1);

    data.flags[2] = true;
    bool contain2 = data.contains(2);

    return (contain1, contain2);
  }
}

上面的例子,我们将库Set附着到了库的数据结构Set.Data上,这样,这个数据结构的实例就有了对应函数。附着带来另一个好处是,能将调用对象自动做为第一个参数,比如data.contains(1);,会将data做为第一个参数,再拼上参数1作为第二个参数,一起传入contains()方法,这样写起来更自然和简捷。

使用using A for *可以将库函数附着到任意的类型上。

pragma solidity ^0.4.0;

//用于测试,无任何意义
library Utils{
  function toUint(uint a) returns (uint){
    return a;
  }
  
  function toUint() returns (uint){
    return 0;
  }
  
  
  function toByte32(bytes32 b) returns (bytes32){
    return b;
  }
}

contract LibraryUsingForAny{
  //可以附加到做任意的类型
  using Utils for *;

  function call() returns (uint, bytes32){
    uint i = 10;
    bytes32 b = "abc";

    return (i.toUint(), b.toByte32());
  }
}

在上面的例子上,我们写了一个无意义的工具类Utils,库函数返回某个类型的本身,用于测试附着在多个不同类型上的情况。我们发现using Utils for *;可以将库附着在任意类上,甚至包括基本类型。

使用using A for *的方式,在调用时,默认会将实例传入作为第一个参数,故而i.toUint()实际不会调用Utils.toUint(),而是调用Utils.toUint(uint a)

如果调用时,第一个参数不能对应匹配,比如,我们将上面例子中的bytes32类型的b调用库函数toUint(),由于不存在toUint(bytes32)方法,会报错Member "toUint" not found or not visible after argument-dependent lookup in bytes32

In both situations, all functions, even those where the type of the first parameter does not match the type of the object, are attached. The type is checked at the point the function is called and function overload resolution is performed.

文档上关于附加这块说,即便第一个参数匹配不上,也会附加这个方法。但我自己亲测后发现无法验证这段话。首先,附加后,如果参数不对应,无法访问对应的方法(因为默认调用时,至少带一个参数,调用的实例作为第一个参数)。其次,当在调用的类型及库函数中都找不到对应的时候,就直接编译不通过了。

指令using A for B;只在当前合约中有效,也许后面会放开这个限制,可以允许在全局空间以库函数方式扩展类型的功能。

另一个需要注意的是,库函数调用是EVM函数调用,当你传入memory类型或值类型时,即便是self变量都会创建一个对应的拷贝。只有在是storage的引用变量时,才不会传递变量拷贝。

库函数与外部调用

文档上好像说,使用using A for B可以省掉外部函数调用的开销,对此,暂时存疑。问题参见: https://ethereum.stackexchange.com/questions/18196/implement-solidity-library-with-no-overhead-of-external-function-calls

库函数的限制

  • 不能有状态变量
  • 不能继承,也不能被继承
  • 不能接收ether

后续可能取消这些限制。

其它

使用库合约的合约,可以将库合约视为隐式的父合约(base contracts),当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库L有函数f(),使用L.f()即可访问。此外,internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal的调用惯例,这意味着所有internal类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点,internal的库函数的代码和从其中调用的所有函数将被拉取(pull into)到调用合约中,然后执行一个普通的JUMP来代替DELEGATECALL

关于作者

专注基于以太坊(Ethereum)的相关区块链(Blockchain)技术,了解以太坊,Solidity,Truffle,web3.js。

个人博客: http://tryblockchain.org
版权所有,转载注明出处

参考资料

友情链接: 区块链技术中文社区