原文:How Rust Solved Dependency Hellhtml
每隔一段时间我就会参与一个关于依赖管理和版本的对话,一般是在工做中,其中会出现“依赖地狱”的主题。若是你对这个术语不熟悉,那么我建议你查一下。简要总结多是:“处理应用程序依赖版本和依赖冲突所带来的挫败感”。带着这个,让咱们先得到关于依赖解析的一些技术。git
在讨论包应该具备哪一种依赖关系以及哪些依赖关系可能致使问题时,本主题一般会进入讨论。做为一个真实的例子,在 Widen Enterprises,咱们有一个内部的,可重用的Java框架,它由几个软件包组成,为咱们提供了建立许多内部服务的基础(若是你愿意的话,微服务)。这很好,可是若是你想建立一个依赖于框架中某些东西的可重用共享代码库呢?若是你尝试在应用程序中使用这样的库,最终可能会获得以下依赖关系图:github
就像在这个例子中同样,每当你试图在服务中使用库时,你的服务和库极可能依赖于不一样版本的框架,这就是“依赖地狱”的开始。安全
如今,在这一点上,一个好的开发平台将为你提供如下两种选择的组合:markdown
framework
版本21.1.1
和21.2.0
相互冲突。这两个看起来都合理,对吧?若是两个软件包确实彼此不兼容,那么咱们根本没法在不修改其中一个的状况下将它们一块儿使用。这是一个艰难的状况,但替代方案每每更糟糕。事实上,Java是不应学习的一个很好的例子:架构
app
升级到framework 21.2.0
。这看起来像是一个双输的状况,因此你能够想象,这对添加依赖项很是不利,而且使之成为一个事实上的策略,除了实际的应用程序以外什么都不容许依赖咱们的核心框架。app
在进行这些讨论时,我会常常提到这是一个不适用于全部语言的问题,做为一个例子,Rust“解决”了这个问题。我经常拿Rust如何解决世界上全部的问题开玩笑,但在那里一般有一个真实的核心。所以,当我说Rust“解决”了这个问题以及它是如何工做的时候,让咱们深刻了解一下个人意思。composer
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