一:背景

1. 讲故事

在 .NET AOT 编程中,难免会在 泛型,序列化,以及反射的问题上纠结和反复纠错尝试,这篇我们就来好好聊一聊相关的处理方案。

二:常见问题解决

1. 泛型问题

研究过泛型的朋友应该都知道,从开放类型上产下来的封闭类型往往会有单独的 MethodTable,并共用 EEClass,对于值类型的泛型相当于是不同的个体,如果在 AOT Compiler 的过程中没有单独产生这样的个体信息,自然在运行时就会报错,这么说可能有点懵,举一个简单的例子。


internal class Program
{
static void Main(string[] args)
{
var type = Type.GetType(Console.ReadLine()); try
{
var mylist = typeof(List<>).MakeGenericType(type); var instance = Activator.CreateInstance(mylist);
int count = (int)mylist.GetProperty("Count").GetValue(instance);
Console.WriteLine(count);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
} public class Location
{
}

从上图看直接抛了一个异常,主要原因在于 Location 被踢出了依赖图,那怎么办呢?很显然可以直接 new List<Location> 到依赖图中,但在代码中直接new是非常具有侵入性的操作,那如何让侵入性更小呢?自然就是借助 AOT 独有的 rd (Runtime Directives) 这种xml机制,具体可参见:https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/rd-xml-format.md

rd机制非常强大,大概如下:

1)可以指定程序集,类型,方法作为编译图的根节点使用,和 ILLink 有部分融合。

2)可以手工的进行泛型初始化,也可以将泛型下的某方法作为根节点使用。

3)为Marshal和Delegate提供Pinvoke支持。

在 ilc 源码中是用 compilationRoots 来承载rd过去的根节点,可以一探究竟。


foreach (var rdXmlFilePath in Get(_command.RdXmlFilePaths))
{
compilationRoots.Add(new RdXmlRootProvider(typeSystemContext, rdXmlFilePath));
}

有了这些知识就可以在 rd.xml 中实例化 List<Location> 了,参考如下:


<?xml version="1.0" encoding="utf-8" ?>
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<Assembly Name="Example_21_1">
<Type Name="System.Collections.Generic.List`1[[Example_21_1.Location,Example_21_1]]" Dynamic="Required All" />
</Assembly>
</Application>
</Directives>

同时在 csproj 做一下引入即可。


<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>
</Project>

执行之后如下,要注意一点的是 Dynamic="Required All" 它可以把 List<Location> 下的所有方法和字段都注入到了依赖图中,比如下图中的 Count 属性方法。

2. 序列化问题

序列化会涉及到大量的反射,而反射又需要得到大量的元数据支持,所以很多第三方的Json序列化无法实现,不过官方提供的Json序列化借助于 SourceGenerator 将原来 dll 中的元数据迁移到了硬编码中,从而变相的实现了AOT的Json序列化,参考代码如下:


namespace Example_21_1
{
internal class Program
{
static void Main(string[] args)
{
var person = new Person()
{
Name = "john",
Age = 30,
BirthDate = new DateTime(1993, 5, 15),
Gender = "Mail"
}; var jsonString = JsonSerializer.Serialize(person,
SourceGenerationContext.Default.Person); Console.WriteLine(jsonString);
Console.ReadLine();
}
}
} [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Person))]
internal partial class SourceGenerationContext : JsonSerializerContext { } public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string Gender { get; set; }
}

当用 VS 调试的时候,你会发现多了一个 SourceGenerationContext.Person.g.cs 文件,并且用 properties 数组承载了 Person 的元数据,截图如下:

3. 反射问题

反射其实也是一个比较纠结的问题,简单的反射AOT编译器能够轻松推测,但稍微需要上下文关联的就搞不定了,毕竟涉及到上下文关联需要大量的算力,而目前的AOT编译本身就比较慢了,所以暂时没有做支持,相信后续的版本会有所改进吧,接下来举一个例子演示下。


internal class Program
{
static void Main(string[] args)
{
Invoke(typeof(Person)); Console.ReadLine();
} static void Invoke(Type type)
{
var props = type.GetProperties(); foreach (var prop in props)
{
Console.WriteLine(prop);
}
}
} public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string Gender { get; set; }
}

这段代码在 AOT中是提取不出属性的,因为 Invoke(typeof(Person));type.GetProperties 之间隔了一个 Type type 参数,虽然我们肉眼能知道这个代码的意图,但 ilc 的深度优先它不知道你需要 Person中的什么,所以它只保留了 Person 本身,如果你想直面观测的话,可以这样做:

  1. <PublishAot>true</PublishAot> 改成 <PublishTrimmed>true</PublishTrimmed>
  2. 使用 dotnet publish 发布。
  3. 使用ILSPY观测。

截图如下,可以看到 Person 空空如也。

有了这个底子就比较简单了,为了让 Person 保留属性,可以傻乎乎的用 DynamicallyAccessedMembers 来告诉AOT我到底想要什么,比如 PublicProperties 就是所有的属性,当然也可以设置为 ALL。


static void Invoke([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
{
var props = type.GetProperties(); foreach (var prop in props)
{
Console.WriteLine(prop);
}
}

如果要想侵入性更小的话,可以使用 TrimmerRootDescriptor 这种外来的 xml 进行更高级别的定制,比如我不想要 Gender 字段 ,具体参考官方链接:https://github.com/dotnet/runtime/blob/main/docs/tools/illink/data-formats.md#xml-examples


<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcGenerateMapFile>true</IlcGenerateMapFile>
</PropertyGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="link.xml" />
</ItemGroup>
</Project>

然后就是 xml 配置。


<?xml version="1.0" encoding="utf-8" ?>
<linker>
<assembly fullname="Example_21_1">
<type fullname="Example_21_1.Person">
<property signature="System.Int32 Age" />
<property signature="System.String Name" />
<property signature="System.DateTime BirthDate" />
</type>
</assembly>
</linker>

从下图看,一切都是那么完美。

三:总结

在将程序发布成AOT的过程中,总会遇到这样或者那样的坑,这篇算是提供点理论基础给后来者吧,同时 Runtime Directives 这种无侵入的实例化方式,很值得关注哈。

AOT漫谈专题(第六篇): C# AOT 的泛型,序列化,反射问题的更多相关文章

  1. PerfView专题 (第六篇):如何洞察 C# 中 GC 的变化

    一:背景 在洞察 GC 方面,我觉得市面上没有任何一款工具可以和 PerfView 相提并论,这也是为什么我会在 WinDbg 之外还要学习这么一款工具的原因,这篇我们先简单聊聊 PerfView 到 ...

  2. 第十六篇 .NET高级技术之序列化

    .net framework的类库中提供了三个可以用于序列化和反序列化的类,分别为BinaryFormatter.SoapFormatter和XmlSerializer. BinaryFormatte ...

  3. python 面向对象专题(六):元类type、反射、函数与类的区别、特殊的双下方法

    目录 Python面向对象06 /元类type.反射.函数与类的区别.特殊的双下方法 1. 元类type 2. 反射 3. 函数与类的区别 4. 特殊的双下方法 1. 元类type type:获取对象 ...

  4. 无线安全专题_攻击篇--MAC泛洪攻击

    上一篇讲解了无线安全专题_攻击篇--干扰通信,没在首页待多长时间就被拿下了,看来之后不能只是讲解攻击实战,还要进行技术原理和防御方法的讲解.本篇讲解的是局域网内的MAC泛洪攻击,这种攻击方式主要目的是 ...

  5. 解剖SQLSERVER 第十六篇 OrcaMDF RawDatabase --MDF文件的瑞士军刀(译)

    解剖SQLSERVER 第十六篇 OrcaMDF RawDatabase --MDF文件的瑞士军刀(译) http://improve.dk/orcamdf-rawdatabase-a-swiss-a ...

  6. 解剖SQLSERVER 第六篇 对OrcaMDF的系统测试里避免regressions(译)

    解剖SQLSERVER 第六篇  对OrcaMDF的系统测试里避免regressions (译) http://improve.dk/avoiding-regressions-in-orcamdf-b ...

  7. Python之路【第十六篇】:Django【基础篇】

    Python之路[第十六篇]:Django[基础篇]   Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为:大而全,框架本身集成了O ...

  8. 第六篇 :微信公众平台开发实战Java版之如何自定义微信公众号菜单

    我们来了解一下 自定义菜单创建接口: http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_to ...

  9. RabbitMQ学习总结 第六篇:Topic类型的exchange

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  10. 第六篇 Replication:合并复制-发布

    本篇文章是SQL Server Replication系列的第六篇,详细内容请参考原文. 合并复制,类似于事务复制,包括一个发布服务器,一个分发服务器和一个或多个订阅服务器.每一个发布服务器上可以定义 ...

随机推荐

  1. 3.2.0 版本预告!远程日志解决 Worker 故障获取不到日志的问题

    Apache DolphinScheduler 3.2.0 版本已经呼之欲出,8 月 中下旬,这个大版本就要和用户见面了.为了让大家提前了解到此版本更新的主要内容,我们已经制作了几期视频和内容做了大致 ...

  2. 2024-08-14:用go语言,给定两个长度分别为n和m的整数数组nums和changeIndices,下标从1开始。初始时,nums 中所有下标均未标记。 从第1秒到第m秒,每秒可以选择以下四种操

    2024-08-14:用go语言,给定两个长度分别为n和m的整数数组nums和changeIndices,下标从1开始.初始时,nums 中所有下标均未标记. 从第1秒到第m秒,每秒可以选择以下四种操 ...

  3. Linux 进程编程入门

    关于进程和线程的关系,之前一口君写过这几篇文章,大家可以参考下. 本文从头带着大家一起学习Linux进程 <搞懂进程组.会话.控制终端关系,才能明白守护进程干嘛的?> <[粉丝问答6 ...

  4. 在Ubuntu 16.04 LTS服务器上安装FreeRADIUS和Daloradius的方法

    FreeRADIUS 为AAA Radius Linux下开源解决方案,DaloRadius为图形化web管理工具. freeradius一般用来进行账户认证管理,记账管理,常见的电信运营商的宽带账户 ...

  5. Elsa V3学习之脚本

    在前面的文章中,可以看到我们经常使用JS脚本来获取变量的值.在Elsa中是支持多种脚本的,最常用的基本是JS脚本和C#脚本. 本文来介绍以下这两个脚本使用. Javascript 在ELSA中的jav ...

  6. 如何使用Typora写出自己的第一个博客

    markdown的使用说明 一.标题 语法:# 这是一级标题 ##这是二级标题...... 代码: # 这是一级标题 ## 这是二级标题 快捷键: Ctrl+数字:数字1-6可以快速将选中的文本调成对 ...

  7. armbian挂载sd卡记录

    mkdir -p /mnt/mmctouch  /etc/init.d/mount.shvim /etc/init.d/mount.sh内容见图mount /dev/mmcblk1p1 /mnt/mm ...

  8. 以Top-Down思维去解决问题——递归

    目录 递归的基础 递归的底层实现(不是重点) 递归的应用场景 编程中 两种解决问题的思维 自下而上(Bottom-Up) 自上而下(Top-Down) 自上而下的思考过程--求和案例 台阶问题 案例 ...

  9. IDEA maven 项目 如何获取项目离线运行所需的全部依赖( .m2格式)

    背景:maven项目要将整个项目的依赖移植到某无法联网服务器进行测试,需要项目离线运行所需的全部依赖 步骤: 1. 首先需要有项目源码,解压后,使用IDEA Open Project 2. 在Sett ...

  10. vue 报错 Component template should contain exactly one root element. If you are using v-if on multiple elements

    vue 报错 Component template should contain exactly one root element. If you are using v-if on multiple ...