.NET NativeAOT 指南

.NET NativeAOT 指南

随着 .NET 8 的发布,一种新的“时尚”应用模型 NativeAOT 开始在各种真实世界的应用中广泛使用。

除了对 NativeAOT 工具链的基本使用外,“NativeAOT”一词还带有原生世界的所有限制,因此您必须知道如何处理这些问题才能正确使用它。

在这篇博客中,我将讨论它们。

基本用法

PublishAot=true

通常,它可以是:

dotnet publish -c Release -r win-x64 /p:PublishAot=true
win-x64linux-x64osx-arm64
bin/Release///publish

关于编译

在讨论使用 NativeAOT 时可能遇到的各种问题的解决方案之前,我们需要稍微深入一点,看看 NativeAOT 是如何编译代码的。

我们经常听说 NativeAOT 会剪裁掉没有被使用的代码。而实际上,它并不像 IL 剪裁那样从程序集中剪裁掉不必要的代码,而是只编译代码中引用的东西。

NativeAOT 编译包括两个阶段:

  1. 扫描 IL 代码,构建整个程序视图(一个依赖图),其中包含所有需要编译的必要依赖节点。
  2. 对依赖图中的每个方法进行实际的编译,生成代码。

请注意,在编译过程中可能会出现一些“延迟”的依赖,因此上述两个阶段可能会交错出现。

这意味着,在分析过程中没有被计算为依赖的任何东西最终都不会被编译。

反射

依赖图是在编译期间静态构建的,这也意味着任何无法静态分析的东西都不会被编译。不幸的是,反射,即在不事先告诉编译器的情况下在运行时获取东西,正是编译器无法弄清楚的一件事。

NativeAOT 编译器有一些能力可以根据编译时的字面量来推断出反射调用需要什么东西。

例如:

var type = Type.GetType(“Foo”);
Activator.CreateInstance(type); class Foo
{

public Foo() =&gt; Console.WriteLine(&#34;Foo instantiated&#34;);<br/>

}

FooFooFooFoo
Foo instantiated

但是如果我们将代码改为如下:

var type = Type.GetType(Console.ReadLine());
Activator.CreateInstance(type); class Foo
{

public Foo() =&gt; Console.WriteLine(&#34;Foo instantiated&#34;);<br/>

}

FooFoo
Unhandled Exception: System.ArgumentNullException: Value cannot be null. (Parameter ‘type’)
at System.ArgumentNullException.Throw(String) + 0x2b
at System.ActivatorImplementation.CreateInstance(Type, Boolean) + 0xe7

FooFootypenull

此外,依赖分析是精确到单个方法的,这意味着即使一个类型被认为是一个依赖,如果该类型中的某个方法没有被使用,该方法也不会被包含在代码生成中。

TrimmerRootAssemblyTrimmerRootAssembly

但是涉及泛型的情况就不是这样了。

动态泛型实例化

在 .NET 中,我们有泛型,编译器会为每个非共享的泛型类型和方法生成不同的代码。

Point
struct Point&lt;T&gt;
{

public T X, Y;<br/>

}

PointPointPoint.XPoint.YintPointPoint.XPoint.Yfloat

通常情况下,这不会导致任何问题,因为编译器可以静态地找出你在代码中使用的所有实例化,直到你试图使用反射来构造一个泛型类型或一个泛型方法:

var type = Type.GetType(Console.ReadLine());
var pointType = typeof(Point&lt;&gt;).MakeGenericType(type);
PointPointPoint
intfloatPoint&lt;&gt;PointPoint
TrimmerRootAssemblyPointPoint

解决方案

既然我们已经找出了在 NativeAOT 下可能发生的潜在问题,让我们来谈谈解决方案。

在其他地方使用它

最简单的想法是,我们可以通过在代码中使用它来让编译器知道我们需要什么。

例如,对于代码

var type = Type.GetType(Console.ReadLine());
var pointType = typeof(Point&lt;&gt;).MakeGenericType(type);
PointPoint
// 我们使用一个永远为假的条件来确保代码不会被执行
// 因为我们只想让编译器知道依赖关系
// 注意,如果我们在这里简单地使用一个 if (false)
// 这个分支会被编译器完全移除,因为它是多余的
// 所以,让我们在这里使用一个不平凡但不可能的条件
if (DateTime.Now.Year &lt; 0)
{

var list = new List&lt;Type&gt;();<br/>
list.Add(typeof(Point&lt;int&gt;));<br/>
list.Add(typeof(Point&lt;float&gt;));<br/>

}

DynamicDependency

DynamicDependencyAttribute

所以我们可以利用它来告诉编译器:“如果 A 被包含在依赖图中,那么也添加 B”。

下面是一个例子:

class Foo
{

readonly Type t = typeof(Bar);

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]

public void A()<br/>
{<br/>
    foreach (var prop in t.GetProperties())<br/>
    {<br/>
        Console.WriteLine(prop);<br/>
    }<br/>
}<br/>

} class Bar
{

public int X { get; set; }<br/>
public int Y { get; set; }<br/>

}

Foo.ABarBar

这个属性还有许多重载,可以接受不同的参数来适应不同的用例,您可以在这里查看文档。

Foo.AUnconditionalSuppressMessage
class Foo
{

readonly Type t = typeof(Bar);

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]

[UnconditionalSuppressMessage(&#34;ReflectionAnalysis&#34;, &#34;IL2080&#34;,<br/>
    Justification = &#34;The properties of Bar have been preserved by DynamicDependency.&#34;)]<br/>
public void A()<br/>
{<br/>
    foreach (var prop in t.GetProperties())<br/>
    {<br/>
        Console.WriteLine(prop);<br/>
    }<br/>
}<br/>

}

DynamicallyAccessedMembers

TTType
void Foo&lt;T&gt;()
{

foreach (var prop in typeof(T).GetProperties())<br/>
{<br/>
    Console.WriteLine(prop);<br/>
}<br/>

} class Bar
{

public int X { get; set; }<br/>
public int Y { get; set; }<br/>

}

FooBarFooFooTBarBar
DynamicallyAccessedMembersT
void Foo&lt;[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T&gt;()
{

// ...<br/>

}

FooTBar
Type
void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t)
{

foreach (var prop in t.GetProperties())<br/>
{<br/>
    Console.WriteLine(prop);<br/>
}<br/>

}

string
Foo(“Bar”);
void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] string s)
{

foreach (var prop in Type.GetType(s).GetProperties())<br/>
{<br/>
    Console.WriteLine(prop);<br/>
}<br/>

}

DynamicDependency
class Foo
{

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]<br/>
readonly Type t = typeof(Bar);

public void A()

{<br/>
    foreach (var prop in t.GetProperties())<br/>
    {<br/>
        Console.WriteLine(prop);<br/>
    }<br/>
}<br/>

}

顺便说一句,这也是推荐的方法。

TrimmerRootAssembly

TrimmerRootAssembly
&lt;ItemGroup&gt;

&lt;TrimmerRootAssembly Include=&#34;MyAssembly&#34; /&gt;<br/>

&lt;/ItemGroup&gt;

TrimmerRootDescriptor

TrimmerRootDescriptor
&lt;ItemGroup&gt;

&lt;TrimmerRootDescriptor Include=&#34;link.xml&#34; /&gt;<br/>

&lt;/ItemGroup&gt;

TrimmerRootDescriptor 文件的文档和格式可以在这里找到。

Runtime Directives

对于泛型实例化的情况,它们无法通过 TrimmerRootAssembly 或 TrimmerRootDescriptor 来解决,这里需要一个包含 runtime directives 的文件来告诉编译器需要编译的东西。

&lt;ItemGroup&gt;

&lt;RdXmlFile Include=&#34;rd.xml&#34; /&gt;<br/>

&lt;/ItemGroup&gt;

rd.xml
rd.xml
DynamicallyAccessedMembersDynamicDependency

结语

NativeAOT 是 .NET 中一个非常棒和强大的工具。有了 NativeAOT,你可以以可预测的性能构建你的应用,同时节省资源(更低的内存占用和更小的二进制大小)。

它还将 .NET 带到了不允许 JIT 编译器的平台,例如 iOS 和主机平台。此外,它还使 .NET 能够运行在嵌入式设备甚至裸机设备上(例如在 UEFI 上运行)。

在使用工具之前了解工具,这样你会节省很多时间。