dotnet7 aot編譯實戰

0 起因

這段日子看到dotnet7-rc1發佈,我對NativeAot功能比較感興趣,如果aot成功,這意味了我們的dotnet程序在防破解的上直接指數級提高。我隨手使用asp.netcore-7.0模板創建了一個默認的web程序,發現aot發佈出來,web服務完全使用,這是之前那些preview版本做不到的。想到fastgithub本質上也是基於asp.netcore-6.0框架的項目,於是走上fastgithub的aot改造之路。

1 改造步驟

1.1 升級框架

將所有項目的TargetFramework值改為7.0,fastgithub使用Directory.Build.props,所以我只需要在Directory.Build.props文件修改一個地方,所有項目生效了。

1.2 升級nuget包

所有項目的nuget包進行升級,像有些是6.0.x版本的,如果有7.0.x-rc.x.x的更新包,就升級到最新rc版本。

1.3 json序列化

如果您的使用JsonSerializer序列化了內部未公開的類型,則需要改為JsonSerializerContext(源代碼生成)方式,比如我在想序列化下面的EndPointItem類型的實例,需要如下改進:

private record EndPointItem(string Host, int Port);

[JsonSerializable(typeof(EndPointItem[]))]
[JsonSourceGenerationOptions(
    WriteIndented = true,
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
private partial class EndPointItemsContext : JsonSerializerContext
{
}
 var utf8Json = JsonSerializer.SerializeToUtf8Bytes(endPointItems, EndPointItemsContext.Default.EndPointItemArray);

2 aot發佈

我發佈在vs上進行發佈時有問題,我們需要在使用cli來發佈,cli發佈還能為我們提供更多的編譯信息輸出。

2.1 單文件的發佈命令

set output=./publish
if exist "%output%" rd /S /Q "%output%"
dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r win-x64 -o "%output%/fastgithub_win-x64" ./FastGithub/FastGithub.csproj

aot編譯之後也是單個文件,所以如果您的程序使用PublishSingleFile模式發佈不能正常運行的話,就不用試着aot發佈了。

2.2 aot發佈的命令

set output=./publish
if exist "%output%" rd /S /Q "%output%"
dotnet publish -c Release /p:PublishAot=true /p:PublishTrimmed=true --self-contained -r win-x64 -o "%output%/fastgithub_win-x64" ./FastGithub/FastGithub.csproj

我們只需要把之前的PublishSingleFile改為PublishAot,他們兩個不能同時設置為true。經過幾分鐘的滿屏黃色警告之後,我們終於得到aot版本的40MB左右的fastgtihub.exe,迫不及待地運行了fastgithub.exe,不幸的是程序運行異常:

Unhandled Exception: System.TypeInitializationException: A type initializer threw an exception. To determine which type, inspect the InnerException's StackTrace property.
 ---> System.TypeInitializationException: A type initializer threw an exception. To determine which type, inspect the InnerException's StackTrace property.
 ---> System.NotSupportedException: 'Org.BouncyCastle.Security.DigestUtilities+DigestAlgorithm[]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see //aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.General.TypeUnifier.WithVerifiedTypeHandle(RuntimeArrayTypeInfo, RuntimeTypeInfo) + 0x5b
   at System.Array.InternalCreate(RuntimeType, Int32, Int32*, Int32*) + 0x5c
   at System.Array.CreateInstance(Type, Int32) + 0x46
   at System.RuntimeType.GetEnumValues() + 0x86
   at Org.BouncyCastle.Utilities.Enums.GetArbitraryValue(Type enumType) + 0xa
   at Org.BouncyCastle.Security.DigestUtilities..cctor() + 0x86

2.3 嘗試解決BouncyCastle

BouncyCastle是用於生成ca證書和服務器證書的第三方庫,在dotnet6時或以前,我們沒有其它庫可以完成這個功能。以上的異常大概是提示了DigestUtilities這個類型的某個內部私有類型被裁剪了,所以無法創建這個已裁剪掉類型的數組類型。我想到可以給項目的ItemGroup加上<TrimmerRootAssembly Include="BouncyCastle.Crypto" />,讓這個程序集不要裁剪,然後再進行新一輪aot編譯,不幸的是這次是編譯時異常:

CVTRES : fatal error CVT1103: 無法讀取文件 [D:\github\FastGithub\FastGithub\FastGithub.csproj]
LINK : fatal error LNK1123: 轉換到 COFF 期間失敗: 文件無效或損壞 [D:\github\FastGithub\FastGithub\FastGithub.csproj]
C:\Program Files\dotnet\sdk\7.0.100-rc.1.22431.12\Sdks\Microsoft.DotNet.ILCompiler\build\Microsoft.NETCore.Native.targe
ts(349,5): error MSB3073: 命令「"C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Tools\MSVC\14.34.31721\bin\Hostx
64\x64\link.exe" @"obj\Release\net7.0\win-x64\native\link.rsp"」已退出,代碼為 1123。 [D:\github\FastGithub\FastGithub\FastGithu
b.csproj]

2.4 移除BouncyCastle

迫於無奈,我們必須移除對BouncyCastle的依賴,轉為使用基礎庫來實現證書生成,這方面幾乎沒有任何可以查到有幫助的資料,我花了整整一天來改造,感興趣證書生成的同學,可以參考CertGenerator.cs。去掉BouncyCastle之後再aot發佈,程序可以運行起來了,沒有任何異常,但是發現程序沒有攔截任何流量。

2.5 查找程序不幹活的原因

由於沒有任何的異常輸出,咱也不知道是啥情況,現在使用debug模式繼續aot發佈,然後運行fastgithub.exe,在vs附加到fastgithub進程,下斷點分析。經過一路跟蹤,我發現如下一個分支,總是進入return邏輯:

var domain = question.Name;
if (this.fastGithubConfig.IsMatch(question.Name.ToString()) == false)
{
    return;
}

我想看看fastGithubConfig現在是什麼值,為什麼總是不匹配,但是經過aot之後,無法發現fastGithubConfig這個局部變量,而函數內的變量,也不再是crl類型,而是一種為調試而存在的代理類型一樣,可看的信息也很少。
於是我加入大量的log,通過log看看fastGithubConfig是什麼值,最後發現是配置綁定到Options的字典類型屬性時,綁定不成功(但也沒有任何異常或日誌)。

2.6 解決配置綁定到字典的問題

這個問題咱實在不知道怎麼解決,那就github上發起問題吧:services.Configure(configuration) failure at PublishAot,果然回復很積極,告訴咱們目前可以在任意調用的函數加上[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<string, DomainConfig>))]。經過這麼修改之後,配置綁定到Options生效了。

3 後續

經過這麼一個實際項目aot之後,我對aot有了初步的了解,個人覺得aot基本可以用小型程序的發佈,期待到dotnet8之後,NativeAot變成沒有坑。