原文:How Rust Solved Dependency Hellhtml
每隔一段時間我就會參與一個關於依賴管理和版本的對話,一般是在工做中,其中會出現「依賴地獄」的主題。若是你對這個術語不熟悉,那麼我建議你查一下。簡要總結多是:「處理應用程序依賴版本和依賴衝突所帶來的挫敗感」。帶着這個,讓咱們先得到關於依賴解析的一些技術。git
在討論包應該具備哪一種依賴關係以及哪些依賴關係可能致使問題時,本主題一般會進入討論。做爲一個真實的例子,在 Widen Enterprises,咱們有一個內部的,可重用的Java框架,它由幾個軟件包組成,爲咱們提供了建立許多內部服務的基礎(若是你願意的話,微服務)。這很好,可是若是你想建立一個依賴於框架中某些東西的可重用共享代碼庫呢?若是你嘗試在應用程序中使用這樣的庫,最終可能會獲得以下依賴關係圖:github
就像在這個例子中同樣,每當你試圖在服務中使用庫時,你的服務和庫極可能依賴於不一樣版本的框架,這就是「依賴地獄」的開始。安全
如今,在這一點上,一個好的開發平臺將爲你提供如下兩種選擇的組合:架構
framework
版本21.1.1
和21.2.0
相互衝突。這兩個看起來都合理,對吧?若是兩個軟件包確實彼此不兼容,那麼咱們根本沒法在不修改其中一個的狀況下將它們一塊兒使用。這是一個艱難的狀況,但替代方案每每更糟糕。事實上,Java是不應學習的一個很好的例子:app
app
升級到framework 21.2.0
。這看起來像是一個雙輸的狀況,因此你能夠想象,這對添加依賴項很是不利,而且使之成爲一個事實上的策略,除了實際的應用程序以外什麼都不容許依賴咱們的核心框架。composer
在進行這些討論時,我會常常提到這是一個不適用於全部語言的問題,做爲一個例子,Rust「解決」了這個問題。我經常拿Rust如何解決世界上全部的問題開玩笑,但在那裏一般有一個真實的核心。所以,當我說Rust「解決」了這個問題以及它是如何工做的時候,讓咱們深刻了解一下個人意思。框架
Rust的解決方案涉及至關多的動人的部分,但它基本上歸結爲挑戰咱們在此以前作出的核心假設:ide
最終應用程序中只應存在任何給定包的一個版本。函數
Rust挑戰了這一點,以便重構問題,看看是否有一個在依賴地獄以外更好的解決方案。Rust平臺主要有兩個功能能夠協同工做,爲解決這些依賴問題提供基礎,如今咱們將分別研究並看看最終結果是怎樣的。
難題的第一部分固然是Cargo,Rust官方依賴管理器。Cargo相似於NPM或Maven之類的工具,而且有一些有趣的功能使它成爲一個真正高質量的依賴管理器(這裏我最喜歡的是Composer,一個很是精心設計的PHP依賴管理器)。Cargo負責下載項目依賴的Rust庫,稱爲crates,並協調調用Rust編譯器以得到最終結果。
請注意,crates是編譯器中的第一類構造。這在之後很重要。
與NPM和Composer同樣,Cargo容許你根據語義版本控制的兼容性規則指定項目兼容的一系列依賴項版本。這容許你描述與你的代碼兼容(或可能)兼容的一個或多個版本。例如,我可能會添加
[dependencies]
log = "0.4.*"
複製代碼
到Cargo.toml
文件,代表個人代碼適用於0.4
系列中log
包的任何補丁版本。也許在最終的應用程序中,咱們獲得了這個依賴樹
由於在my-project
中我聲明瞭與log
版本0.4.*
的兼容性,咱們能夠安全地爲log
選擇版本0.4.4
,由於它知足全部要求。(若是log
包遵循語義版本控制的原則,這個原則對於已發佈的庫而言並不老是如此,那麼咱們能夠確信這個發佈不包括任何會破壞咱們代碼的重大更改。)你能夠在Cargo文檔中找到一個更好地解釋版本範圍以及它們如何應用於Cargo。
太棒了,因此咱們能夠選擇知足每一個項目版本要求的最新版本,而不是選擇避開遇到版本衝突或只是選擇更新的版本並祈禱。可是,若是咱們遇到沒法解決的問題,例如:
沒有能夠選擇知足全部要求的log
版本!咱們接下來作什麼?
爲了回答這個問題,咱們須要討論名字修飾。通常來講,名字修飾是一些編譯器用於各類語言的過程,它將符號名稱做爲輸入,並生成一個更簡單的字符串做爲輸出,可用於在連接時消除相似命名符號的歧義。例如,Rust容許你在不一樣模塊之間重用標識符:
mod en {
fn greet() {
println!("Hello");
}
}
mod es {
fn greet() {
println!("Hola");
}
}
複製代碼
這裏咱們有兩個不一樣的函數,名爲greet()
,但固然這很好,由於它們在不一樣的模塊中。這很方便,但一般應用程序二進制格式沒有模塊的概念;相反,全部符號都存在於單個全局命名空間中,很是相似於C中的名稱。因爲greet()
在最終二進制文件中不能顯示兩次,所以編譯器可能使用比源代碼更明確的名稱。例如:
en::greet()
成爲en__greet
es::greet()
成爲es__greet
問題解決了!只要咱們確保這個名字修飾方案是肯定性的而且在編譯期間處處使用,代碼就會知道如何得到正確的函數。
如今這不是一個徹底完整的名字修飾方案,由於咱們尚未考慮不少其餘的東西,好比泛型類型參數,重載等等。此功能也不是Rust獨有的,而且確實在C++和Fortran等語言中使用了很長時間。
名字修飾如何幫助Rust解決依賴地獄?這一切都在Rust的名字管理體系中,這彷佛在我所研究的語言中至關獨特。那麼讓咱們來看看?
在Rust編譯器中查找名字修飾的代碼很簡單;它位於一個名爲symbol_names.rs
的文件中。若是你想學習更多內容,我建議你閱讀這個文件中的註釋,但我會包括重點。彷佛有四個基本組件包含在一個修飾符號名稱中:
使用Cargo時,Cargo自己會將「歧義消除器」提供給編譯器,因此讓咱們看一下compilation_files.rs
包含的內容:
這個複雜系統的最終結果是,即便是不一樣版本的crate中的相同功能也具備不一樣的修飾符號名稱,所以只要每一個組件知道要調用的函數版本,就能夠在單個應用程序中共存。
如今回到咱們以前的「沒法解決的」依賴圖:
藉助依賴範圍的強大功能,以及Cargo和Rust編譯器協同工做,咱們如今能夠經過在咱們的應用程序中包含log 0.5.0
和log 0.4.4
來實際解決此依賴關係圖。app
內部使用log
的任何代碼都將被編譯以達到從0.5.0
版生成的符號,而my-project
中的代碼將使用爲0.4.4
版生成的符號。
如今咱們看到了大局,這實際上看起來很是直觀,並解決了一大堆依賴問題,這些問題會困擾其餘語言的用戶。這個解決方案並不完美:
log 0.5.0
的LogLevel
並將其傳遞給my-project
使用,由於它指望LogLevel
來自log 0.4.4
,而且它們必須被視爲單獨的類型。因爲這些缺點,Cargo僅在須要時才採用這種技術來解決依賴圖。
爲了解決通常用例,這些彷佛值得爲Rust作出權衡,但對於其餘語言,採用這樣的東西可能會更加困難。以Java爲例,Java嚴重依賴於靜態字段和全局狀態,所以簡單地大規模採用Rust的方法確定會增長破壞代碼的次數,而Rust則將全局狀態限制在最低限度。這種設計也沒有對在運行時或反射時加載任意庫進行說明,這二者都是許多其餘語言提供的流行功能。
Rust在編譯和打包方面的精心設計以(主要)無痛依賴管理的形式帶來紅利,這一般消除了可能成爲開發人員在其餘語言中最糟糕的噩夢的整類問題。當我第一次開始玩Rust的時候,我固然很喜歡我所看到的,深刻了解內部,看到宏大的架構,周到的設計,以及合理的權衡取捨對我來講更使人印象深入。這只是其中的一個例子。
即便你沒有使用Rust,但願這會讓你對依賴管理器,編譯器以及他們必須解決的棘手問題給予新的重視。(雖然我鼓勵你至少嘗試一下Rust,固然......)
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI