AOT漫談:C# AOT 的泛型,序列化,反射問題
一、背景
講故事
在 .NET AOT 編程中,難免會在 泛型,序列化,以及反射的問題上糾結(jié)和反復(fù)糾錯嘗試,這篇我們就來好好聊一聊相關(guān)的處理方案。
二、常見問題解決
1. 泛型問題
研究過泛型的朋友應(yīng)該都知道,從開放類型上產(chǎn)下來的封閉類型往往會有單獨(dú)的 MethodTable,并共用 EEClass,對于值類型的泛型相當(dāng)于是不同的個體,如果在 AOT Compiler 的過程中沒有單獨(dú)產(chǎn)生這樣的個體信息,自然在運(yùn)行時就會報錯,這么說可能有點(diǎn)懵,舉一個簡單的例子。
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 獨(dú)有的 rd (Runtime Directives) 這種xml機(jī)制,具體可參見:https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/rd-xml-format.md
rd機(jī)制非常強(qiáng)大,大概如下:
1)可以指定程序集,類型,方法作為編譯圖的根節(jié)點(diǎn)使用,和 ILLink 有部分融合。2)可以手工的進(jìn)行泛型初始化,也可以將泛型下的某方法作為根節(jié)點(diǎn)使用。3)為Marshal和Delegate提供Pinvoke支持。
在 ilc 源碼中是用 compilationRoots 來承載rd過去的根節(jié)點(diǎn),可以一探究竟。
foreach (var rdXmlFilePath in Get(_command.RdXmlFilePaths))
{
compilationRoots.Add(new RdXmlRootProvider(typeSystemContext, rdXmlFilePath));
}
有了這些知識就可以在 rd.xml 中實(shí)例化 List<Location> 了,參考如下:
<?xml versinotallow="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>
執(zhí)行之后如下,要注意一點(diǎn)的是 Dynamic="Required All" 它可以把 List<Location> 下的所有方法和字段都注入到了依賴圖中,比如下圖中的 Count 屬性方法。
圖片
2. 序列化問題
序列化會涉及到大量的反射,而反射又需要得到大量的元數(shù)據(jù)支持,所以很多第三方的Json序列化無法實(shí)現(xiàn),不過官方提供的Json序列化借助于 SourceGenerator 將原來 dll 中的元數(shù)據(jù)遷移到了硬編碼中,從而變相的實(shí)現(xiàn)了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; }
}
當(dāng)用 VS 調(diào)試的時候,你會發(fā)現(xiàn)多了一個 SourceGenerationContext.Person.g.cs 文件,并且用 properties 數(shù)組承載了 Person 的元數(shù)據(jù),截圖如下:
圖片
3. 反射問題
反射其實(shí)也是一個比較糾結(jié)的問題,簡單的反射AOT編譯器能夠輕松推測,但稍微需要上下文關(guān)聯(lián)的就搞不定了,畢竟涉及到上下文關(guān)聯(lián)需要大量的算力,而目前的AOT編譯本身就比較慢了,所以暫時沒有做支持,相信后續(xù)的版本會有所改進(jìn)吧,接下來舉一個例子演示下。
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中是提取不出屬性的,因?yàn)?Invoke(typeof(Person)); 和 type.GetProperties 之間隔了一個 Type type 參數(shù),雖然我們?nèi)庋勰苤肋@個代碼的意圖,但 ilc 的深度優(yōu)先它不知道你需要 Person中的什么,所以它只保留了 Person 本身,如果你想直面觀測的話,可以這樣做:
- 將 <PublishAot>true</PublishAot> 改成 <PublishTrimmed>true</PublishTrimmed>
- 使用 dotnet publish 發(fā)布。
- 使用ILSPY觀測。
截圖如下,可以看到 Person 空空如也。
圖片
有了這個底子就比較簡單了,為了讓 Person 保留屬性,可以傻乎乎的用 DynamicallyAccessedMembers 來告訴AOT我到底想要什么,比如 PublicProperties 就是所有的屬性,當(dāng)然也可以設(shè)置為 ALL。
static void Invoke([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
{
var props = type.GetProperties();
foreach (var prop in props)
{
Console.WriteLine(prop);
}
}
如果要想侵入性更小的話,可以使用 TrimmerRootDescriptor 這種外來的 xml 進(jìn)行更高級別的定制,比如我不想要 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 versinotallow="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>
從下圖看,一切都是那么完美。
圖片