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(<libraryName>, <address>)
  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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 最近听了混沌上的很多产品课,都在表达一个观点,我们的产品都是用我们全部的努力,然后做到极致。 为什么做产品一定把目...
    马惠良阅读 442评论 0 0
  • 在我出发前,便有苏州的朋友告知我,这里会下雨,但却并没有阻止我此次出行的计划。出站时,果然下着蒙蒙细雨,按照在火车...
    烛流萤阅读 1,523评论 0 0
  • 前言 汪汪不是一只狗 是一位美丽的女子 她有一个简单的梦 每天坐在一大厦的南墙边 对着太阳笑…… 第一节 黑夜的魔...
    云方文阅读 210评论 0 0
  • 今天孩子作业不积极,我们也没及时检查。 中午,放了一会儿风,出去玩就忘了一切,作业抛九霄云外了。 下午背课文,没有...
    辛本朔阅读 130评论 0 0