Truffle Linker 的解释

定义

Solidity在语法层面,定义了共享库的概念,而Truffle Linker(链接器)就是在编译环节之后,将共享库和其它合约链接到一起的工具。看完这篇文章,我们就会知道运行完Truffle deploy命令生成出的./build/contracts/.json文件,其蕴含的信息更像是Linux下ELF格式/Windows下PE格式的可执行文件。因为它包含的不仅有编译后的二进制代码和描述这些代码的ABI,还有重定向之后的合约及其所依赖的共享库的地址。

源代码

MetaCoin使用sendCoin转移代币,并依赖函数库ConvertLib完成汇率转换。

ConvertLib.sol

pragma solidity >=0.4.21 <0.6.0;

library ConvertLib{
    function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount)
    {
        return amount * conversionRate;
    }
}

MetaCoin.sol

pragma solidity >=0.4.21 <0.6.0;

import "./ConvertLib.sol";

contract MetaCoin {
    mapping (address => uint) balances;

    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    constructor() public {
        balances[tx.origin] = 10000;
    }

    function sendCoin(address receiver, uint amount) public returns(bool sufficient) {
        if (balances[msg.sender] < amount) return false;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Transfer(msg.sender, receiver, amount);
        return true;
    }

    function getBalanceInEth(address addr) public view returns(uint){
        return ConvertLib.convert(getBalance(addr),2);
    }

    function getBalance(address addr) public view returns(uint) {
        return balances[addr];
    }
}

Truffle Linker的调用时机

Truffle Linker何时被执行?大家可能很快就能猜出答案:运行truffle deploy的时候。确实没错,不过这里面还有些可以深入探索的细节,顺着这些细节也可以了解一下Truffle的设计思路。

分析得从最近的路开始

老规矩,按照上篇《Truffle Provider的构造与解释》[1]我们知道了truffle deploy一定会运行truffle-migrate/migration.js文件,下面这段代码尤其重要。

// migration.js -> _load(options, context, deployer, resolver, callback)
const migrateFn = fn(deployer, options.network, accounts);
await self._deploy(options, deployer, resolver, migrateFn, callback);

上回说到,fn其实是Truffle项目目录migrations/各个迁移js脚本中的module.exports暴露出来的函数,这个函数也是声明链接的地方,我们以MetaCoin为例,其中涉及将库ConvertLib链接到合约MetaCoin上的过程。

// migrations/2_add_metacoin.js
var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");

module.exports = function(deployer) {
    deployer.deploy(ConvertLib);
    deployer.link(ConvertLib, MetaCoin);
    deployer.deploy(MetaCoin);
}

有问题的地方就有贯穿理解的机会

好奇心能帮助发现问题。建立问题和知识点之间的依赖关系,有利于梳理出陌生问题的脉络,我们知道对问题的正确认知是解决问题的前提。

在仔细阅读上面两段代码的过程中,我产生了三点疑问。

1. deploy和link真的执行了?

基于前面提到的等同关系,我们做个简单的带入,刚才提到的fn就是MetaCoin迁移脚本中的这段代码:

// fn(deployer, options.network, accounts) equals the following.
function(deployer) {
    deployer.deploy(ConvertLib);
    deployer.link(ConvertLib, MetaCoin);
    deployer.deploy(MetaCoin);
}

也就是说,一旦fn(...)被调用,函数体中所有代码都会被立即执行。

deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);

看上去,deploylink都被立即执行了。那么问题来了,既然已经执行部署和链接的命令,下面这行代码又为什么会存在呢?

await self._deploy(options, deployer, resolver, migrateFn, callback);

要想弄清楚这个问题,方法至少有两个。其一,查看self._deploy(...)的内容,梳理传入参数migrateFn是如何被使用的,然后反向推理依赖脉络。其二,直接进入deploy()的实现代码一探究竟。我们依次来过,先看前者。

await deployer.start();

// Allow migrations method to be async and
// deploy to use await
if (migrateFn && migrateFn.then !== undefined){
    await deployer.then(() => migrateFn);
}

deployer.start()似乎暗示着部署到这里才刚刚开始。if条件语句中的判断则暗示migrateFn可能是一个Promise实例。

我们接着看deploy()的源码,它位于项目truffle-deployer的index.js文件中,

deploy() {
    const args = Array.prototype.slice.call(arguments);
    const contract = args.shift();
    return this.queueOrExec(this.executeDeployment(contract, args, this));
}

这里的executeDeployment(...)是实际执行部署任务的函数,而函数queueOrExec(...)就是用来延迟执行的关键点。

queueOrExec(fn) {
    var self = this;
    return (this.chain.started == true)
      ? new Promise(accept => accept()).then(fn)
      : this.chain.then(fn);
}

原来,deployer.deploy()函数只是将执行部署的任务包裹在了一个函数中,然后将这个函数放进一个队列当中,使用Promise.then(fn)的方法入队。回过头来,我们再看self._deploy(...)中的代码就不难理解了。

// truffle-migrate/migration.js -> self._deploy
await deployer.start();

// truffle-deployer/deferredchain.js -> start
DeferredChain.prototype.start = function() {
  this.started = true;
  this.chain = this.chain.then(this._done);
  this._accept();
  return this.await;
};

deployer.start()函数首先把started标志位置成启动,再将this._done放到这个chain的末尾,注意this._done其实是最后this.await这个Promise对象的resolve方法,所以这行代码代表返回值this.await将拥有整个Promise执行链条最后的结果。this._accept()是队列头的resolve方法,它的调用将会触发整个队列依次出队,即.then方法的不断执行。

2. artifacts哪里来的?

当看到var ConvertLib = artifacts.require("./ConvertLib.sol");,我们自然而然以为这是NodeJS的模块导入语法,但是仔细一看显然不是。所以这个artifacts到底是哪儿来的呢?它的作用是什么?

去调用点最近的地方找它的定义。在项目truffle-require下的require.js文件里,可以找到context的定义,其中就有artifacts的声明,如下:

var context = {
    ...
    artifacts: options.resolver,
    ...
}

沿着这条线向上找,就会触及truffle-migrate项目里migration.js中的函数_load(.., resolver, callback) -> run(options, callback)。再往上就找到了truffle-core/command中migrate.js的这条赋值语句。

// truffle-core/migrate.js -> run(options, done)
var Migrate = require("truffle-migrate");
var Resolver = require("truffle-resolver");

config.resolver = new Resolver(config);
...
Migrate.run(config, callback);

语句Migrate.run(config, callback)是部署函数的调用入口。所以,最终在truffle-resolver项目下的fs.js中找到了artifacts.require("./ConvertLib.sol")的定义和实现。

// truffle-resolver/fs.js -> require(import_path, search_path)
...
var contract_name = this.getContractName(import_path, search_path);
...
var result = fs.readFileSync(path.join(search_path, contract_name + ".json"), "utf8");
return JSON.parse(result);

上面代码的返回结果是./build/contracts/.json文件中的对象,例如:MetaCoin.json. 当函数返回后,这个JSON对象会被包装成contract对象,如下:

// truffle-resolver -> require(import_path, search_path)
var contract = require("truffle-contract");

var result = source.require(import_path, search_path); //source = fs
if (result) {
  var abstraction = contract(result); //包装成contract
  provision(abstraction, self.options);
  return abstraction;
}

可以看到,不管是deployer.deploy(ConvertLib)还是deployer.link(ConvertLib, MetaCoin),它们接收的参数都是truffle-contract对象。这个知识点很重要,尤其是帮助理解接下来我们要讲到的链接(link)工作。

3. link到底做了什么?

代码 deployer.link(ConvertLib, MetaCoin)到底是如何工作的?首先找到link函数的定义处,它位于在truffle-deployer项目下的源码目录中有一个linker.js文件,link函数接收library和destinations等参数。

link: async function(library, destinations, deployer) {
    ...
    destination.link(library);
}

根据我们之前得到的启示,destination和library都是truffle-contract对象,所以contract.link(lib)函数的定义位于项目truffle-contract中。我们找到一个名为contract.js的文件,开头处有如下解构语句。

const {
  bootstrap,
  constructorMethods,
  properties
} = require("./contract/index");

此处的constructorMethods就是关键所在。这个对象中的link方法便是我们要找的函数。

link: function(name, address) {
  var constructor = this;

  // Case: Contract.link(instance)
  if (typeof name === "function") {
    var contract = name;
    if (contract.isDeployed() === false) {
         throw new Error("Cannot link contract without an address.");
    }
    ...
    this.link(contract.contractName, contract.address);
    ...
    return;
  }

  // Case: Contract.link(, 
) if (this._json.networks[this.network_id] == null) { this._json.networks[this.network_id] = { events: {}, links: {} }; } ... this.network.links[name] = address; }

这末尾的一条语句this.network.links[name] = address就是将Library的名字及其部署地址链接到一起的操作。最终就会产出MetaCoin.json的“链接”版本。

// MetaCoin.json
"networks": {
        "1548668200785": {
            "events": {},
            "links": {
                "ConvertLib": "0x5e2947D1DaB06Cbd10Eb258205522c15B3c9b7E9"
            },
            "address": "0x099A1d107cE2BEE9F23590fa2AB55E9e1FEE03aA",
            "transactionHash": "0x22108cd4c4b240467a20d155fb1828db693b1f771b107d8dc5e2cd066d7d58cc"
        }
    }

正像我前面提到的,MetaCoin.json文件更像是Linux ELF和Windows上的PE文件,这两种格式的文件会维护全局变量或函数的符号表用于链接时进行重定向。所谓重定向,就是把符号替换成地址。到这里,Truffle还剩下重定向这步操作没有完成。

Linker的重定向机制

Solidity的编译器solc其实也是链接器[2]。当我们用solc --link生成二进制代码时,这段二进制代码就会被解析成unlinked,也就是说引用Library的地方都是占位符。如果我们要做链接重定向,那么得传入--libraries "file.sol:Math:0x1234567890123456789012345678901234567890这样的参数。solc就会将那些占位符替换成真正的地址。

可以想象,Truffle无非帮我们自动地完成这样的步骤。说到这里,我们其实可以理解,Solidity目前只支持静态链接,准确的说应该是静态共享链接。因为Library其实完全是共享单元,类似常驻内存的共享程序(share object)。如果有一些链接和加载的基础,不难看出这里面的问题,比如共享程序升级了,那些依赖它的合约该如何升级呢?这是个有趣的思考题。

小结

Solidity的编译,链接和部署(装载)是区块链背景下的系统工程,具有不可变数据库的特征,但是又比数据库的迁移工作复杂很多。而对我而言,把敏捷软件开发的实践接入到区块链应用开发当中是当务之急,思考、类比和归纳或许是条路。


  1. Truffle Provider 构造及其解释 ↩

  2. Linker ↩

你可能感兴趣的:(Truffle Linker 的解释)