.NET中的引用程序集

Intro

在 .NET 里有一种特殊的程序集叫做 ReferenceAssembly(引用程序集),不知道大家是不是知道,在 VS 里 F12 一个程序集的时候,可能会遇到方法实现里都是 throw null; 的情况,这种情况就和 ReferenceAssembly 有关系。.

What

引用程序集(Reference Assemblies) 是一种特殊类型的程序集,它只包含表示库的公共 API 所需的最少元数据量。它们包括在生成工具中引用程序集时所需的所有成员的声明,但不包括所有成员实现以及对其 API 协定没有明显影响的私有成员的声明。相比较下,常规程序集称为“实现程序集” (implementation assemblies)。

引用程序集无法用于执行,但可以将它们作为编译器输入进行传递,其传递方式与实现程序集相同。引用程序集通常随特定平台或库的软件开发工具包 (SDK) 一起分发,就是说引用程序集是可以用于编译的,但是不能用于执行。

引用程序集是对相关概念“仅元数据程序集”的扩展。仅包含元数据的程序集会将方法主体替换为一个 throw null 主体,但包括除匿名类型以外的所有成员。使用 throw null 主体(而非不使用主体)的原因在于,这样做可以运行和传递 PEVerify(从而验证元数据的完整性)。

引用程序集进一步从仅包含元数据的程序集中删除元数据(私有成员):

  • 引用程序集只包含在 API 外围应用中所需的引用。实际程序集可能包含与特定实现相关的其他引用。例如,class C { private void M() { dynamic d = 1; ... } } 的引用程序集不引用 dynamic 所需的任何类型。
  • 删除私有函数成员(方法、属性和事件),前提是这不会对编译造成显著影响。如果没有 InternalsVisibleTo 属性,则会同时删除内部函数成员。

引用程序集中的元数据继续保留以下信息:

  • 所有类型,包括专用类型和嵌套类型。
  • 所有属性(甚至是内部属性)。
  • 所有虚拟方法。
  • 显式接口实现。
  • 显式实现的属性和事件,因为它们的访问器是虚拟的。
  • 结构的所有字段。

如果要将引用程序集与 NuGet 包一起分发,必须将它们包含在包目录下的 ref\ 子目录中(而不是用于实现程序集的 lib\ 子目录中)。

Why

既然我们有实现程序集,为什么还要有引用程序集呢?

使用引用程序集,开发人员可以生成面向特定库版本的程序,而无需具有该版本的完整实现程序集。因为不包含实现,引用程序集会更小一些,加载和解析都会更快一些。

引用程序集还可以表示协定(契约),即一组不与具体实现程序集对应的 API。此类引用程序集称为“协定程序集” (Contract Assembly),可用于面向支持同一组 API 的多个平台。例如,.NET Standard 提供协定程序集 netstandard .dll ,它表示在不同的 .NET 平台之间共享的一组公共 API。这些 API 的实现包含在不同平台上的不同程序集中,例如 .NET Framework 上的 mscorlib.dll 或 .NET Core 上的 System.Private.CoreLib.dll 。面向 .NET Standard 的库可以在支持 .NET Standard 的所有平台上运行。

这类似于我们和第三方的开发者约定的 API 规范,我们可以先给出 API 的请求和响应而无需提供实现以不 block 第三方开发者的进度,毕竟他们只关心 API 是什么样的而不关心实现。

若要使用项目中的某些 API,必须添加对其程序集的引用。可以将引用添加到实现程序集,也可以将其添加到引用程序集。建议在引用程序集可用时使用它。这样做可确保仅使用目标版本中受支持的 API 成员,即供 API 设计人员使用。使用引用程序集可确保不依赖于实现详细信息。

How

.NET Framework 库的引用程序集与目标包一起分发。可以通过下载独立安装程序或在 Visual Studio 安装程序中选择组件来获取它们。有关详细信息,请参阅安装面向开发人员的 .NET Framework。对于 .NET Core 和 .NET Standard,引用程序集将在必要时(通过 NuGet)进行自动下载和引用。

在 .NET Core 3.0 之前很多程序集都是发布 NuGet 包的,对于 .NET Core 3.0 和更高版本,核心框架的引用程序集位于 Microsoft.NETCore.App.Ref 包中,一般情况下是不需要的,因为引用程序集也会随着 .NET SDK 一起发布,你可以在 SDK 的安装目录下的 packs 目录下找到对应框架版本的引用程序集

下面是我电脑上 SDK 里的框架引用程序集的一个示例

.NET中的引用程序集

对于引用程序集只能用于编译,这种程序集会有一些特殊,反编译的话会看到有一个 ReferenceAssembly 的程序集 Attribute,下面是我从上面的目录中找的 System.Text.Json 的反编译结果,可以看到有一个 ReferenceAssembly 的 attribute

.NET中的引用程序集

Reference Assembly

再看一下 JsonNode 的实现

.NET中的引用程序集

我们再找一个实现的程序集对比一下

.NET中的引用程序集

Implementation assembly

.NET中的引用程序集

由于它们不包含任何实现,因此无法加载引用程序集用于执行。如果尝试这样做,则会导致 System.BadImageFormatException,可能会遇到 Reference assemblies can only be loaded in the Reflection-only loader context. 这样的错误。

如果要检查引用程序集的内容,你可将其加载到 .NET Framework 中的仅反射上下文中(使用 Assembly.ReflectionOnlyLoad 方法),或者加载到 .NET Core 中的 MetadataLoadContext。

More

经常看源码的童鞋,一定会注意到,dotnet/runtime 中很多的类库的结构都是类似下面这样的

.NET中的引用程序集

runtime library structure

大家会看到第一个目录是 ref,也就是用来生成引用程序集的,src 则是包含了实现的项目源码,test 则是一些测试用例 https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/

ref 项目引用的其他项目也都是直接引用的 ref 项目 https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.csproj

.NET中的引用程序集

查看 ref 项目的代码,可以发现和反编译的效果是一样的,都是空实现或者 throw null https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs#L7

.NET中的引用程序集

最近在做 dotnet-exec 这个小工具的时候就遇到了引用程序集的问题,起初没怎么理解这个引用程序集,在编译代码时使用的是引用程序集,在执行代码时也是用的引用程序集,在执行时 load 程序集的时候就报了前面提到的

BadImageException Reference assemblies can only be loaded in the Reflection-only loader context.

在看到 Youtube 上这个介绍 Reference Assembly 的视频(https://www.youtube.com/watch?v=EBpY1UMHDY8&list=PLRAdsfhKI4OX1cBGL2IXuEq1yzpDyKlwf&index=1&t=3s)之后才恍然大悟,原来如此。。。虽然视频是以 .NET Framework 为例讲解的,.NET Core 也类似,感兴趣的可以看一下

在 VS 里经常会遇到 F12 之后看到的实现都是 throw null,猜测也是因为这个原因,在编译时 VS 使用的是引用程序集来提高性能

最后有没有好奇 ref 项目和 src 项目的差别在哪里?表面上看 ref 项目文件里的东西好像没什么特别的啊,利用了之前我们提到过的 Directory.Build.props 来为大多数项目统一配置了,感兴趣的同学可以根据下面的链接自己探索一下

https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Directory.Build.props#L8

https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/eng/referenceAssemblies.props#L22

.NET中的引用程序集