Solidity語言學習筆記————36、 庫

FLy_鵬程萬里發表於2018-07-08

庫(Libraries)

庫與合約類似,但它的目的是在一個指定的地址,且僅部署一次,然後通過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.containsSet.removeSet.insert都會編譯為以DELEGATECALL的方式呼叫external的合約和庫。如果使用庫,需要注意的是一個實實在在的外部函式呼叫發生了。儘管msg.sendermsg.valuethis還會保持它們在此呼叫中的值(在Homestead之前,由於實際使用的是CALLCODEmsg.sendermsg.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個符號替換為庫的十六進位制地址。

對比普通合約來說,庫的限制:

  • 無狀態變數。
  • 不能繼承或被繼承
  • 不能接收ether。

這些限制將來也可能被解除。

庫的呼叫保護(Call Protection For Libraries)

正如在引言中提到的,如果庫的程式碼是使用CALL 而不是DELEGATECALL 或CALLCODE來執行的,那麼除非呼叫一個viewpure 函式,否則它將恢復。

EVM沒有提供一種直接的方法來檢測是否使用CALL來呼叫它,但是合約可以使用ADDRESS 操作碼來查詢它當前執行的“何處”。生成的程式碼將此地址與建立時使用的地址進行比較,以確定呼叫模式。

更具體地說,庫的執行時程式碼總是從編譯時為20位元組的0的推送指令開始。當部署程式碼執行時,該常數由當前地址在記憶體中被替換,並且該修改的程式碼被儲存在合約中。在執行時,這會導致部署時間地址是第一個被推到堆疊上的常量,排程器程式碼將當前地址與此常量對任何非檢視和非純函式進行比較。

相關文章