库(Libraries)

库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL(Homestead之前是用CALLCODE)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this将会指向到调用合约,而且可以访问调用合约的存储(storage)。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量(state variables),否则除此之外,没有任何方法去知道这些状态变量。

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

下面的例子展示了如何使用库(后续在using for章节有一个更适合的实现Set的例子)。

pragma solidity ^0.4.0;

library Set {
  // We define a new struct datatype that will be used to
  // hold its data in the calling contract.
  struct Data { mapping(uint => bool) flags; }

  // 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)
      returns (bool)
  {
      if (self.flags[value])
          return false; // already there
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      returns (bool)
  {
      if (!self.flags[value])
          return false; // not there
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      returns (bool)
  {
      return self.flags[value];
  }
}


contract C {
    Set.Data knownValues;

    function register(uint value) {
        // The library functions can be called without a
        // specific instance of the library, since the
        // "instance" will be the current contract.
        if (!Set.insert(knownValues, value))
            throw;
    }
    // In this contract, we can also directly access knownValues.flags, if we want.
}

上面的例子中:

  • Library定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为self
  • 另外一个需要留意的是上例中self的类型是storage,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。
  • 库函数的使用不需要实例化,c.register中可以看出是直接使用Set.insert。但实际上当前的这个合约本身就是它的一个实例。
  • 这个例子中,c可以直接访问,knownValues。虽然这个值主要是被库函数使用的。

当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用storage类型的参数,还可以在任何位置有多个storage的引用类型的参数。

调用Set.containsSet.removeSet.insert都会编译为以DELEGATECALL的方式调用external的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管msg.sendermsg.valuethis还会保持它们在此调用中的值(在Homestead之前,由于实际使用的是CALLCODEmsg.sendermsg.value会变化)。

下面的例子演示了如何使用memory类型和内部函数(inernal function),来实现一个自定义类型,但不会用到外部函数调用(external function)

pragma solidity ^0.4.0;

library BigInt {
    struct bigint {
        uint[] limbs;
    }

    function fromUint(uint x) internal returns (bigint r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint _a, bigint _b) internal 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 returns (uint) {
        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
    }

    function max(uint a, uint b) private returns (uint) {
        return a > b ? a : b;
    }
}


contract C {
    using BigInt for BigInt.bigint;

    function f() {
        var x = BigInt.fromUint(7);
        var y = BigInt.fromUint(uint(-1));
        var z = x.add(y);
    }
}

因为编译器并不知道库最终部署的地址。这些地址须由linker填进最终的字节码中(使用命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符_Set___(其中Set是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。

对比普通合约来说,库的限制:

  • 状态变量(state variables)
  • 不能继承或被继承
  • 不能接收ether

这些限制将来也可能被解除!

附着库(Using for)

指令using A for B;用来附着库里定义的函数(从库A)到任意类型B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python中的self变量一样。

using A for *的效果是,库A中的函数被附着在做任意的类型上。

在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。

using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。

下面我们来换个方式重写set的例子。

pragma solidity ^0.4.0;

// This is the same code as before, just without comments
library Set {
  struct Data { mapping(uint => bool) flags; }

  function insert(Data storage self, uint value)
      returns (bool)
  {
      if (self.flags[value])
        return false; // already there
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      returns (bool)
  {
      if (!self.flags[value])
          return false; // not there
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      returns (bool)
  {
      return self.flags[value];
  }
}


contract C {
    using Set for Set.Data; // this is the crucial change
    Set.Data knownValues;

    function register(uint value) {
        // Here, all variables of type Set.Data have
        // corresponding member functions.
        // The following function call is identical to
        // Set.insert(knownValues, value)
        if (!knownValues.insert(value))
            throw;
    }
}

我们也可以通过这种方式来扩展基本类型(elementary types)

pragma solidity ^0.4.0;

library Search {
    function indexOf(uint[] storage self, uint value) returns (uint) {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return uint(-1);
    }
}


contract C {
    using Search for uint[];
    uint[] data;

    function append(uint value) {
        data.push(value);
    }

    function replace(uint _old, uint _new) {
        // This performs the library function call
        uint index = data.indexOf(_old);
        if (index == uint(-1))
            data.push(_new);
        else
            data[index] = _new;
    }
}

需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是memory类型的,或者是值类型(vaue types),那么仅会传一份拷贝,即使是self变量。变通之法就是使用存储(storage)类型的变量,这样就不会拷贝内容。

处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。