Truffle Linker 的解釋
定義
Solidity在語法層面,定義了共享庫的概念,而Truffle Linker(連結器)就是在編譯環節之後,將共享庫和其它合約連結到一起的工具。看完這篇文章,我們就會知道執行完Truffle deploy
命令生成出的./build/contracts/.json
檔案,其蘊含的資訊更像是Linux下ELF格式/Windows下PE格式的可執行檔案。因為它包含的不僅有編譯後的二進位制程式碼和描述這些程式碼的ABI,還有重定向之後的合約及其所依賴的共享庫的地址。
Truffle Linker的呼叫時機
Truffle Linker何時被執行?大家可能很快就能猜出答案:執行truffle deploy
的時候。確實沒錯,不過這裡面還有些可以深入探索的細節,順著這些細節也可以瞭解一下Truffle的設計思路。
分析得從最近的路開始
老規矩,按照上篇《Truffle Provider的構造與解釋》我們知道了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);
看上去,deploy
和link
都被立即執行了。那麼問題來了,既然已經執行部署和連結的命令,下面這行程式碼又為什麼會存在呢?
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其實也是連結器。當我們用solc --link
生成二進位制程式碼時,這段二進位制程式碼就會被解析成unlinked,也就是說引用Library的地方都是佔位符。如果我們要做連結重定向,那麼得傳入--libraries "file.sol:Math:0x1234567890123456789012345678901234567890
這樣的引數。solc就會將那些佔位符替換成真正的地址。
可以想象,Truffle無非幫我們自動地完成這樣的步驟。說到這裡,我們其實可以理解,Solidity目前只支援靜態連結,準確的說應該是靜態共享連結。因為Library其實完全是共享單元,類似常駐記憶體的共享程式(share object)。如果有一些連結和載入的基礎,不難看出這裡面的問題,比如共享程序升級了,那些依賴它的合約該如何升級呢?這是個有趣的思考題。
小結
Solidity的編譯,連結和部署(裝載)是區塊鏈背景下的系統工程,具有不可變資料庫的特徵,但是又比資料庫的遷移工作複雜很多。而對我而言,把敏捷軟體開發的實踐接入到區塊鏈應用開發當中是當務之急,思考、類比和歸納或許是條路。