7.1 什么是库
库是一种特殊的合约其只需要部署一次,然后就可以加载指定的地址合约库并利用其对外提供的公共函数服务。库合约在底层依赖于delegatecall操作码来实现函数跳转。其实现大致思路为先将需要的代码拷贝到主调用合约的运行空间然后通过无条件跳转jump指令跳转到目标代码处执行。其代码执行空间是在调用合约本身因此不发生上下文执行环境切换只是在调用合约内部进行跳转,本质是将指定地址的代码片段拷贝到调用合约运行空间以达到高效切换和调用的目的。
库也是一种合约和调用合约是独立的代码块,尽管两者最终运行在同样运行空间。虽然可以通过this指向调用合约并访问其存储空间的状态变量,但由于库函数是提供公共服务的通用合约,为了松耦合设计库函数最好访问调用合约明确提供的参数。
库合约可以看做是调用合约的隐式父合约尽管没有明确的继承关系,但使用方式类似。假设名称为A库有个名称为d的函数其调用方式为A.d(),同时库定义的所有internal函数对直接和间接继承子类都可见,如同真正的祖先父类合约一样。
相对于成熟的传统编程语言,目前solidity语言本身的表达能力还不够强大,也缺少很多标准功能库的支持,同时受限于区块链编程的特殊性以及运行环境的分布式异步执行使得编程的时候要格外注意。不过在可预见的将来随着solidity的快速发展和壮大,会有越来越多的第三方功能库甚至是标准库的出现来简化日常编程和提高安全性。同时solidity中除了通过面向对象设计的方式达到代码重用的目的外,使用库也是一种高效的代码重用的方式而且两者常常有机结合起来。
7.2 库的定义与使用
库的定义与合约一致包括函数定义及其参数列表和返回值定义等,唯一的区别就是以关键字library开头,下面我们定义一个简单的数学辅助库,示意代码如下:
library YxMath {
function add(uint256 a, uint256 b)
public pure returns(uint256){
return a+b;
}
}
contract LibTest {
function doTest1(uint256 a, uint256 b)
public pure returns(uint256) {
return YxMath.add(a,b);
}
}
如代码所示当库函数代码可知时通过这种方式使用库函数不需要对库进行实例化,当前合约本身就是库的一个实例。在对主调用合约进行部署时其依赖的库会自动部署,如下所示:
creation of library browser/HelloWorld.sol:YxMath pending...
[vm] from:0xca3...a733c, to:YxMath.(constructor), value:0 wei,
data:0x60a...00029, 0 logs, hash:0x119...2caeb
[vm] from:0xca3...a733c, to:LibTest.(constructor), value:0 wei,
data:0x608...d0029, 0 logs, hash:0xfef...b7c51
库的实际链接发生在字节码层级。当合约编译时其所依赖库会暂时用类似1234_C_______11223344这样的占位符合表示,其中0x11223344是具体调用函数的ABI选择器器值,然后再对所依赖的库进行实际部署并得到部署后的链上合约地址,在调用合约底层代码中替换掉之前的占位符后再进行实际的函数调用。
前面我们介绍过库函数在调用时不发生运行环境的上下文切换,在编程实践中具有重要的特殊含义。具体就是运行时消息调用者合约地址等不发生改变,示意代码如下:
library YxMath {
function info() public view returns(address, address, uint256) {
return (address(this), msg.sender, address(this).balance);
}
}
contract LibTest {
function deposit() public payable {}
function myInfo() public view returns(address, address, uint256) {
return (address(this), msg.sender, address(this).balance);
}
function libInfo() public view returns(address, address, uint256) {
return YxMath.info();
}
}
实际运行时函数myinfo和libinfo返回值始终一样,这也证明了合约调用库函数时不发生上下文的切换一切都在原来的合约中。
库中也可以进行自定义类型定义,其定义的类型可以被外部调用合约感知和使用。这个功能支持相当重要特别是在实现通用目的的合约中间件时非常有用。当库中定义了其期望的数据类型后,后续库中所有的操作都针对这个特定类型,外面合约只要将数据组装为库指定类型,所有库的功能都可以无缝使用。示意代码如下:
library YxMath {
// uint bcd; //compile err
uint constant MAX_NUM = 123;
struct Item {
uint a;
uint b;
mapping(address => uint) balances;
}
// Item data; //compile err
function handle(uint256 a, uint256 b)
public pure returns(uint256) {
return a + b*2 + a*a + MAX_NUM;
}
}
contract LibTest {
YxMath.Item data;
function myInfo() public pure returns(uint256) {
YxMath.Item memory data;
// return YxMath.MAX_NUM;// compile err
return 1;
}
}
如代码所示库中定义的类型可以被调用合约感知和使用,由于库不拥有自己独立的储存和运行空间因此不能定义状态变量,包括各种自定义类型的变量实例,但可以定义编译期常量。不过需要额外注意的是库中定义的常量只能在库内部使用,不能直接在外部的主调用合约中使用。一个变通办法是在库代码实现中再定义一个返回常量的常量函数供库内部和外部使用,另外函数名和内部定义的常量名最后大体一致,示意代码如下:
library YxMath {
uint constant _MAX_NUM = 123;
function MAX_NUM() public pure returns(uint256) {
return _MAX_NUM;
}
function handle(uint256 a, uint256 b) public pure returns(uint256) {
return a + b*2 + a*a + MAX_NUM();
}
}
contract LibTest {
function myInfo() public pure returns(uint256) {
return YxMath.MAX_NUM();
}
}
通过这种变通方式在库中定义的常量可以无缝的给外部调用合约使用。
网友评论