NET NativeAOT 用法指南

2024-03-01 0 859
目录
  • .NET NativeAOT 指南
    • 基本用法
    • 关于编译
    • 反射
    • 动态泛型实例化
    • 解决方案
      • 在其他地方使用它
      • DynamicDependency
      • DynamicallyAccessedMembers
      • TrimmerRootAssembly
      • TrimmerRootDescriptor
      • Runtime Directives
    • 结语

    .NET NativeAOT 指南

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

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

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

    基本用法

    使用 NativeAOT 非常简单,只需要在发布应用时使用 MSBuild 传递一个属性PublishAot=true即可。

    通常,它可以是:

    dotnet publish -c Release -r win-x64 /p:PublishAot=true

    其中win-x64是运行时标识符,可以替换为linux-x64,osx-arm64或其他平台。您必须指定它,因为 NativeAOT 需要为您指定的运行时标识符生成原生代码。

    然后发布的应用可以在bin/Release/<target framework>/<runtime identifier>/publish中找到

    关于编译

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

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

    NativeAOT 编译包括两个阶段:

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

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

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

    反射

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

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

    例如:

    var type = Type.GetType(\”Foo\”);
    Activator.CreateInstance(type);
    class Foo
    {
    public Foo() => Console.WriteLine(\”Foo instantiated\”);
    }

    上面的反射目标(即Foo)可以被编译器弄清楚,因为编译器可以看到你试图获取类型Foo,所以类型Foo会被标记为一个依赖,这导致Foo被编译到最终的产物中。

    如果你运行这个程序,它会如预期地打印Foo instantiated。

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

    var type = Type.GetType(Console.ReadLine());
    Activator.CreateInstance(type);
    class Foo
    {
    public Foo() => Console.WriteLine(\”Foo instantiated\”);
    }

    现在让我们用 NativeAOT 构建并运行这个程序,然后输入Foo来创建一个Foo的实例。你会立刻得到一个异常:

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

    这是因为编译器无法看到你在哪里使用了Foo,所以它根本不会为Foo生成任何代码,导致这里的type为null。

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

    虽然这可以通过将所有类型和方法添加到依赖图中来解决,这样编译器就会为它们生成代码。这就是TrimmerRootAssembly的作用:通过提供TrimmerRootAssembly,NativeAOT 编译器会将你指定的程序集中的所有东西都作为根。

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

    动态泛型实例化

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

    假设我们有一个类型Point<T>:

    struct Point<T>
    {
    public T X, Y;
    }

    如果我们有一段代码试图使用Point<int>,编译器会为Point<int>生成专门的代码,使得Point.X和Point.Y都是int。如果我们有一个Point<float>,编译器会生成另一个专门的代码,使得Point.X和Point.Y都是float。

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

    var type = Type.GetType(Console.ReadLine());
    var pointType = typeof(Point<>).MakeGenericType(type);

    上面的代码在 NativeAOT 下不会工作,因为编译器无法推断出Point<T>的实例化,所以编译器既不会生成Point<int>的代码,也不会生成Point<float>的代码。

    尽管编译器可以为int,float,甚至泛型类型定义Point<>生成代码,但是如果编译器没有生成Point<int>的实例化代码,你就无法使用Point<int>。

    即使你使用TrimmerRootAssembly来告诉编译器将你的程序集中的所有东西都作为根,也仍然不会为像Point<int>或Point<float>这样的实例化生成代码,因为它们需要根据类型参数来单独构造。

    解决方案

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

    在其他地方使用它

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

    例如,对于代码

    var type = Type.GetType(Console.ReadLine());
    var pointType = typeof(Point<>).MakeGenericType(type);

    只要我们知道我们要使用Point<int>和Point<float>,我们可以在其他地方使用它一次,然后编译器就会为它们生成代码:

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

    DynamicDependency

    我们有一个属性DynamicDependencyAttribute来告诉编译器一个方法依赖于另一个类型或方法。

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

    下面是一个例子:

    class Foo
    {
    readonly Type t = typeof(Bar);
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]
    public void A()
    {
    foreach (var prop in t.GetProperties())
    {
    Console.WriteLine(prop);
    }
    }
    }
    class Bar
    {
    public int X { get; set; }
    public int Y { get; set; }
    }

    现在只要编译器发现有任何代码路径调用了Foo.A,Bar中的所有公共属性都会被添加到依赖图中,这样我们就能够对Bar的每个公共属性进行动态反射调用。

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

    此外,现在我们知道Foo.A中的动态反射在剪裁和 NativeAOT 下不会造成任何问题,我们可以使用UnconditionalSuppressMessage来抑制警告信息,这样在构建过程中就不会再产生任何警告了。

    class Foo
    {
    readonly Type t = typeof(Bar);
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]
    [UnconditionalSuppressMessage(\”ReflectionAnalysis\”, \”IL2080\”,
    Justification = \”The properties of Bar have been preserved by DynamicDependency.\”)]
    public void A()
    {
    foreach (var prop in t.GetProperties())
    {
    Console.WriteLine(prop);
    }
    }
    }

    DynamicallyAccessedMembers

    有时我们试图动态地访问类型T的成员,其中T可以是一个类型参数或一个Type的实例:

    void Foo<T>()
    {
    foreach (var prop in typeof(T).GetProperties())
    {
    Console.WriteLine(prop);
    }
    }
    class Bar
    {
    public int X { get; set; }
    public int Y { get; set; }
    }

    如果我们调用Foo<Bar>,很不幸,这在 NativeAOT 下不会工作。编译器确实看到你是用类型参数Bar调用Foo的,但在Foo<T>的上下文中,编译器不知道T是什么,而且没有其他代码直接使用Bar的属性,所以编译器不会为Bar的属性生成代码。

    这里我们可以使用DynamicallyAccessedMembers来告诉编译器为T的所有公共属性生成代码:

    void Foo<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>()
    {
    // …
    }

    现在当编译器编译调用Foo<Bar>时,它知道T(特别的,这里指Bar)的所有公共属性都应该被视为依赖。

    这个属性也可以应用在一个Type上:

    Foo(typeof(Bar));
    void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t)
    {
    foreach (var prop in t.GetProperties())
    {
    Console.WriteLine(prop);
    }
    }

    甚至在一个string上:

    Foo(\”Bar\”);
    void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] string s)
    {
    foreach (var prop in Type.GetType(s).GetProperties())
    {
    Console.WriteLine(prop);
    }
    }

    所以在这里你可能会发现我们有一个替代方案,用于我们在DynamicDependency一节中提到的代码示例:

    class Foo
    {
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
    readonly Type t = typeof(Bar);
    public void A()
    {
    foreach (var prop in t.GetProperties())
    {
    Console.WriteLine(prop);
    }
    }
    }

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

    TrimmerRootAssembly

    如果你不拥有代码,但你仍然希望代码在 NativeAOT 下工作。你可以尝试使用TrimmerRootAssembly来告诉编译器将一个程序集中的所有类型和方法都作为依赖。但请注意,这种方法不适用于泛型实例化。

    <ItemGroup>
    <TrimmerRootAssembly Include=\”MyAssembly\” />
    </ItemGroup>

    TrimmerRootDescriptor

    对于高级用户,他们可能想要控制从一个程序集中包含什么。在这种情况下,可以指定一个TrimmerRootDescriptor:

    <ItemGroup>
    <TrimmerRootDescriptor Include=\”link.xml\” />
    </ItemGroup>

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

    Runtime Directives

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

    <ItemGroup>
    <RdXmlFile Include=\”rd.xml\” />
    </ItemGroup>

    在rd.xml中,你可以为你的泛型类型和方法指定实例化。

    rd.xml文件的文档和格式可以在这里找到。

    这种方法不推荐,但它可以解决你在使用 NativeAOT 时遇到的一些难题。请在使用 trimmer descriptor 或 runtime directives 之前,总是考虑用DynamicallyAccessedMembers和DynamicDependency来注释你的代码,使其与剪裁/AOT 兼容。

    结语

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

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

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

    到此这篇关于NET NativeAOT 指南的文章就介绍到这了,更多相关NET NativeAOT内容请搜索悠久资源网以前的文章或继续浏览下面的相关文章希望大家以后多多支持悠久资源网!

    您可能感兴趣的文章:

    • .Net Core 2.2升级3.1的避坑指南(小结)
    • ASP.NET Core 3.0迁移的完美避坑指南
    • ASP.NET微信开发(接口指南)
    • .NET验证组件Fluent Validation使用指南

    收藏 (0) 打赏

    感谢您的支持,我会继续努力的!

    打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
    点赞 (0)

    悠久资源 ASP.NET NET NativeAOT 用法指南 https://www.u-9.cn/biancheng/aspnet/181515.html

    常见问题

    相关文章

    发表评论
    暂无评论
    官方客服团队

    为您解决烦忧 - 24小时在线 专业服务