微软是如何创建具有循环引用的程序集的?

111

.NET BCL(基础类库)中存在以下几个程序集之间的循环引用关系:

  • System.dllSystem.Xml.dll
  • System.dllSystem.Configuration.dll
  • System.Xml.dllSystem.Configuration.dll

下面是来自.NET Reflector 的截图,展示了这种情况:

enter image description here

Microsoft 是如何创建这些程序集的仍然是一个谜。是否需要进行特殊的编译过程来允许这种循环引用呢?我想在这里一定有一些有趣的东西正在发生。


2
非常好的问题。我从未花时间去检查这个,但我很想知道答案。确实,Dykam提供了一个合理的答案。 - Noldorin
3
如果这些dll互相依赖,为什么它们没有合并成一个?这样做有没有实际的原因? - Andreas Petersson
1
有趣的问题...我想知道Eric Lippert对此的答案!正如Andreas所说,我想知道为什么他们没有将所有东西放在同一个程序集中... - Thomas Levesque
2
请查看此演示文稿(asmmeta文件):http://www.msakademik.net/academicdays2005/Serge_Lidin.ppt - Mehrdad Afshari
@Mehrdad -- 你提供的链接已经失效了,但是这里有新的链接:https://web.archive.org/web/20100806233100/http://www.msakademik.net/academicdays2005/Serge_Lidin.ppt - J.Merrill
显示剩余2条评论
9个回答

60

我只能告诉你Mono项目是如何实现这一点的,虽然该定理非常简单,但会导致代码混乱。

他们首先编译System.Configuration.dll,不需要引用到System.Xml.dll的部分。之后,他们以正常方式编译System.Xml.dll。现在来了魔法般的时刻。他们重新编译System.Configuration.dll,需要引用到System.Xml.dll的部分。现在,可以成功地编译循环引用的代码。

简而言之:

  • A被编译时没有B的代码和引用。
  • B被编译。
  • A被重新编译。

1
它被 Visual Studio 阻塞了,但可以直接使用命令行编译器(csc.exe)完成。请参见我的回答。 - Alfred Myers
14
我知道。Mono的主要构建系统不是Visual Studio。猜想微软也不是。 - Dykam

36

RBarryYoung和Dykam有所发现。微软使用内部工具,该工具使用ILDASM来反汇编程序集,剥离所有内部/私有内容和方法体,并将IL重新编译(使用ILASM)成为所谓的“脱水程序集”或元数据程序集。每次更改程序集的公共接口时都会执行此操作。

在构建期间,元数据程序集代替实际程序集使用。这样就打破了循环。


1
有趣的回答,你有任何链接吗? - H H
是的,这确实是所做的方式(来自个人经验)。 - Pavel Minaev
如果他们确实剥离了所有的内部和私有内容,那么就不可能对其进行反编译,查看私有类是什么,引用了哪些其他类等。但是除了带有属性 MethodImpl(MethodImplOptions.InternalCall) 的方法之外,这是完全可能的,而这种方法只有很少一部分。也许我误解了你的观点。提供支持你说法的链接可能会有所帮助。 - Abel
真的。我们不需要将程序集A中的内部和私有内容暴露出来,就可以在编译程序集B时引用它的公共内容。对于InternalCall方法的提醒是正确的,但并不适用于此讨论。很抱歉,没有链接。据我所知,这并没有公开记录。 - Srdjan Jovcic
1
它们在构建之后才被强制签名(它们是延迟签名的),因此脱水的程序集未被签名。 - Srdjan Jovcic
显示剩余3条评论

27

按照Dykam所描述的方式是可以完成的,但是Visual Studio会阻止你这样做。

你需要直接使用命令行编译器csc.exe。

  1. csc /target:library ClassA.cs

  2. csc /target:library ClassB.cs /reference:ClassA.dll

  3. csc /target:library ClassA.cs ClassC.cs /reference:ClassB.dll


//ClassA.cs
namespace CircularA {
    public class ClassA {
    }
}


//ClassB.cs
using CircularA;
namespace CircularB {
    public class ClassB : ClassA  {
    }
}


//ClassC.cs
namespace CircularA {
    class ClassC : ClassB {
    }
}

你也可以在Visual Studio中完成这个操作,虽然有些困难。基本方法是使用#if指令,然后在解决方案资源管理器中删除引用,并在第三步中恢复引用。我还想到另一种方法是创建一个第三个项目文件,包含相同的文件但不同的引用。这种方法可以通过指定构建顺序来实现。 - Dykam
据我所知,在这里无法测试。 - Dykam
我真的很想看到那个。从我在这里的实验中,当你尝试添加引用时,IDE会阻止你。 - Alfred Myers
我知道。但是第三个项目没有那个引用和#if符号,并被第二个引用,而第二个又被第一个引用。没有循环。但第三个使用第一个的代码并输出到第一个汇编位置。一个汇编可以很容易地被具有相同规格的另一个所替换。但我认为强名称可能会在这种方法中引起问题。 - Dykam
这有点像Srdjan的回答,不过是另一种方法。 - Dykam

19

只要不使用项目引用,就可以在Visual Studio中轻松完成。请尝试以下操作:

  1. 打开Visual Studio
  2. 创建2个类库项目 "ClassLibrary1" 和 "ClassLibrary2"
  3. 构建
  4. 从ClassLibrary1中浏览到步骤3中创建的dll文件,添加对ClassLibrary2的引用。
  5. 从ClassLibrary2中浏览到步骤3中创建的dll文件,添加对ClassLibrary1的引用。
  6. 再次构建(注意:如果在两个项目中进行更改,您需要构建两次以使两个引用都“新鲜”)

这就是如何做到。但是认真地说...在实际项目中永远不要这样做!如果您这样做,圣诞老人今年将不会给您带来任何礼物。


3
唯一的例外是如果在12月26日至31日期间,并且礼物已经获得。 - Jesse Hufstetler

6

我想可以通过从无环的程序集开始,使用ILMerge将较小的程序集合并成逻辑相关的组来完成。


4

我从未在Windows上尝试过,但我已经在许多编译链接rtl环境中实践了这个方法。首先创建没有交叉引用的存根“目标”,然后进行链接,添加循环引用,最后重新链接。链接器通常不关心循环引用或遵循引用链,它们只关心能够单独解决每个引用。

所以,如果你有两个库A和B需要相互引用,请尝试以下步骤:

  1. 将A链接到B而没有任何引用。
  2. 将B链接到A并带有对A的引用。
  3. 将A链接,并加入对B的引用。

Dykam提出了一个好观点,.Net中是编译而不是链接,但原则仍然相同:创建具有其导出入口的相互引用的源代码,但除一个之外,所有其他源代码的引用都被存根化。以此方式构建它们,然后取消外部引用中的存根并重新构建它们。这应该甚至可以在没有任何特殊工具的情况下完成,事实上,这种方法在我尝试过的每个操作系统上都行得通(大约6个)。当然,自动化这个过程会非常有帮助。


定理是正确的。然而在 .Net 的世界中,链接是动态完成的,不是问题所在。需要这个解决方案的是编译步骤。 - Dykam
抱歉又要纠正你 :P。但在 .Net 世界中,编译时的引用(链接)发生在从特定 ECMA 规范派生的所有内容上。因此包括 Mono、dotGnu 和 .Net,但不包括 Windows 本身。 - Dykam

1
一种可能的方法是使用条件编译(#if)来首先编译一个不依赖于其他程序集的 System.dll,然后编译其他程序集,最后重新编译 System.dll 以包含依赖于 Xml 和 Configuration 的部分。

1
不幸的是,这并不允许您有条件地引用一个程序集(我希望这是可能的,在我的一个项目中它会真正有帮助...) - Thomas Levesque
1
条件引用可以通过编辑 .csproj 文件轻松完成。只需在 <Reference> 元素中添加 Condition 属性即可。 - Daniel

0
从技术上讲,这些库可能根本没有被编译,而是手工组装的。毕竟,这些都是低级别的库。

不完全是。它里面没有太多低级别的东西,只有基础知识。你为什么认为它会是低级别的呢?运行时和corlib是低级别的。相对而言。虽然JIT包含了一些低级别的东西,但仍然是纯C或C++。 - Dykam

0

同意。 asmmeta.exe类似于ildasm,但省略了所有的IL(只有ret)和一些私有内容,尽管有时需要私有内容,例如结构大小。

更一般的想法是多遍构建,这是微软长期以来一直依赖的。

可以将削减后的ildasm输出视为“头”文件,在一个实际上没有头文件的系统中。

首先访问每个目录(使用大规模并行处理!)运行ilasm。 然后访问每个目录(再次使用大规模并行处理)运行csc。 在csc之后,在同一遍中运行类似于ildasm的工具,将原始“头”文件输出回来。 比较它们。如果有任何不匹配,构建就会失败。 开发人员未能更新头文件。 现在修补它已经太晚了,必须重新启动构建(也许使用适当的依赖关系图,大多数目录不会受到影响)。

这也是一种轻松升级版本的方法。 类似于ilasm的代码可以具有版本号名称。 虽然这实际上是多遍构建的一个次要结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接