dotnet7 aot編譯實戰

2022-09-23 15:00:21

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 https://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變成沒有坑。