翼度科技»论坛 编程开发 .net 查看内容

AOT漫谈专题(第六篇): C# AOT 的泛型,序列化,反射问题

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
一:背景

1. 讲故事

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

1. 泛型问题

研究过泛型的朋友应该都知道,从开放类型上产下来的封闭类型往往会有单独的 MethodTable,并共用 EEClass,对于值类型的泛型相当于是不同的个体,如果在 AOT Compiler 的过程中没有单独产生这样的个体信息,自然在运行时就会报错,这么说可能有点懵,举一个简单的例子。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             var type = Type.GetType(Console.ReadLine());
  6.             try
  7.             {
  8.                 var mylist = typeof(List<>).MakeGenericType(type);
  9.                 var instance = Activator.CreateInstance(mylist);
  10.                 int count = (int)mylist.GetProperty("Count").GetValue(instance);
  11.                 Console.WriteLine(count);
  12.             }
  13.             catch (Exception ex)
  14.             {
  15.                 Console.WriteLine(ex.ToString());
  16.                 Console.WriteLine(ex.Message);
  17.             }
  18.             Console.ReadLine();
  19.         }
  20.     }
  21.     public class Location
  22.     {
  23.     }
复制代码

从上图看直接抛了一个异常,主要原因在于 Location 被踢出了依赖图,那怎么办呢?很显然可以直接 new List 到依赖图中,但在代码中直接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过去的根节点,可以一探究竟。
  1. foreach (var rdXmlFilePath in Get(_command.RdXmlFilePaths))
  2. {
  3.     compilationRoots.Add(new RdXmlRootProvider(typeSystemContext, rdXmlFilePath));
  4. }
复制代码
有了这些知识就可以在 rd.xml 中实例化 List 了,参考如下:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  3.         <Application>
  4.                 <Assembly Name="Example_21_1">
  5.                         <Type Name="System.Collections.Generic.List`1[[Example_21_1.Location,Example_21_1]]" Dynamic="Required All" />
  6.                 </Assembly>
  7.         </Application>
  8. </Directives>
复制代码
同时在 csproj 做一下引入即可。
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <OutputType>Exe</OutputType>
  4.                 <TargetFramework>net8.0</TargetFramework>
  5.                 <ImplicitUsings>enable</ImplicitUsings>
  6.                 <Nullable>enable</Nullable>
  7.                 <PublishAot>true</PublishAot>
  8.                 <InvariantGlobalization>true</InvariantGlobalization>
  9.         </PropertyGroup>
  10.         <ItemGroup>
  11.                 <RdXmlFile Include="rd.xml" />
  12.         </ItemGroup>
  13. </Project>
复制代码
执行之后如下,要注意一点的是 Dynamic="Required All" 它可以把 List 下的所有方法和字段都注入到了依赖图中,比如下图中的 Count 属性方法。

2. 序列化问题

序列化会涉及到大量的反射,而反射又需要得到大量的元数据支持,所以很多第三方的Json序列化无法实现,不过官方提供的Json序列化借助于 SourceGenerator 将原来 dll 中的元数据迁移到了硬编码中,从而变相的实现了AOT的Json序列化,参考代码如下:
  1. namespace Example_21_1
  2. {
  3.     internal class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.             var person = new Person()
  8.             {
  9.                 Name = "john",
  10.                 Age = 30,
  11.                 BirthDate = new DateTime(1993, 5, 15),
  12.                 Gender = "Mail"
  13.             };
  14.             var jsonString = JsonSerializer.Serialize(person,
  15.                                             SourceGenerationContext.Default.Person);
  16.             Console.WriteLine(jsonString);
  17.             Console.ReadLine();
  18.         }
  19.     }
  20. }
  21. [JsonSourceGenerationOptions(WriteIndented = true)]
  22. [JsonSerializable(typeof(Person))]
  23. internal partial class SourceGenerationContext : JsonSerializerContext { }
  24. public class Person
  25. {
  26.     public int Age { get; set; }
  27.     public string Name { get; set; }
  28.     public DateTime BirthDate { get; set; }
  29.     public string Gender { get; set; }
  30. }
复制代码
当用 VS 调试的时候,你会发现多了一个 SourceGenerationContext.Person.g.cs 文件,并且用 properties 数组承载了 Person 的元数据,截图如下:

3. 反射问题

反射其实也是一个比较纠结的问题,简单的反射AOT编译器能够轻松推测,但稍微需要上下文关联的就搞不定了,毕竟涉及到上下文关联需要大量的算力,而目前的AOT编译本身就比较慢了,所以暂时没有做支持,相信后续的版本会有所改进吧,接下来举一个例子演示下。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             Invoke(typeof(Person));
  6.             Console.ReadLine();
  7.         }
  8.         static void Invoke(Type type)
  9.         {
  10.             var props = type.GetProperties();
  11.             foreach (var prop in props)
  12.             {
  13.                 Console.WriteLine(prop);
  14.             }
  15.         }
  16.     }
  17.     public class Person
  18.     {
  19.         public int Age { get; set; }
  20.         public string Name { get; set; }
  21.         public DateTime BirthDate { get; set; }
  22.         public string Gender { get; set; }
  23.     }
复制代码
这段代码在 AOT中是提取不出属性的,因为 Invoke(typeof(Person)); 和 type.GetProperties 之间隔了一个 Type type 参数,虽然我们肉眼能知道这个代码的意图,但 ilc 的深度优先它不知道你需要 Person中的什么,所以它只保留了 Person 本身,如果你想直面观测的话,可以这样做:

  • 将 true 改成 true
  • 使用 dotnet publish 发布。
  • 使用ILSPY观测。
截图如下,可以看到 Person 空空如也。

有了这个底子就比较简单了,为了让 Person 保留属性,可以傻乎乎的用 DynamicallyAccessedMembers 来告诉AOT我到底想要什么,比如 PublicProperties 就是所有的属性,当然也可以设置为 ALL。
  1.         static void Invoke([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
  2.         {
  3.             var props = type.GetProperties();
  4.             foreach (var prop in props)
  5.             {
  6.                 Console.WriteLine(prop);
  7.             }
  8.         }
复制代码
如果要想侵入性更小的话,可以使用 TrimmerRootDescriptor 这种外来的 xml 进行更高级别的定制,比如我不想要 Gender 字段 ,具体参考官方链接:https://github.com/dotnet/runtime/blob/main/docs/tools/illink/data-formats.md#xml-examples
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <OutputType>Exe</OutputType>
  4.                 <TargetFramework>net8.0</TargetFramework>
  5.                 <ImplicitUsings>enable</ImplicitUsings>
  6.                 <Nullable>enable</Nullable>
  7.                 <PublishAot>true</PublishAot>
  8.                 <InvariantGlobalization>true</InvariantGlobalization>
  9.                 <IlcGenerateMapFile>true</IlcGenerateMapFile>
  10.         </PropertyGroup>
  11.         <ItemGroup>
  12.                 <TrimmerRootDescriptor Include="link.xml" />
  13.         </ItemGroup>
  14. </Project>
复制代码
然后就是 xml 配置。
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  3.         <Application>
  4.                 <Assembly Name="Example_21_1">
  5.                         <Type Name="System.Collections.Generic.List`1[[Example_21_1.Location,Example_21_1]]" Dynamic="Required All" />
  6.                 </Assembly>
  7.         </Application>
  8. </Directives>                                               
复制代码
从下图看,一切都是那么完美。

三:总结

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

来源:https://www.cnblogs.com/huangxincheng/p/18496142
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具