.NET 7 SDK、Desktop development with C++ workload。
IDEVisual Studio 2022Desktop development with C workload是一个工具集里面包含 C 开发工具需要在Visual Studio Installer中安装如下图红框中所示。创建一个控制台项目首先创建一个 .NET 7 控制台项目名称为CsharpAot。打开项目之后基本代码如图所示我们使用下面的代码做测试public class Program { static void Main() { Console.WriteLine(C# Aot!); Console.ReadKey(); } }体验 AOT 编译这一步可以参考官方网站的更多说明https://learn.microsoft.com/zh-cn/dotnet/core/deploying/native-aot/为了能够让项目发布时使用 AOT 模式需要在项目文件中加上PublishAottrue/PublishAot选项。然后使用 Visual Studio 发布项目。发布项目的配置文件设置需要按照下图进行配置。AOT 跟生成单个文件两个选项不能同时使用因为 AOT 本身就是单个文件。配置完成后点击发布然后打开Release目录会看到如图所示的文件。.exe是独立的可执行文件不需要再依赖.NET Runtime环境这个程序可以放到其他没有安装 .NET 环境的机器中运行。然后删除以下三个文件CsharpAot.exp CsharpAot.lib CsharpAot.pdb光用.exe即可运行其他是调试符号等文件不是必需的。剩下CsharpAot.exe文件后启动这个程序C# 调用库函数这一部分的代码示例是从笔者的一个开源项目中抽取出来的这个项目封装了一些获取系统资源的接口以及快速接入 Prometheus 监控。不过很久没有更新了最近没啥动力更新读者可以点击这里了解一下这个项目https://github.com/whuanle/CZGL.SystemInfo/tree/net6.0/src/CZGL.SystemInfo/Memory因为后续代码需要所以现在请开启 “允许不安全代码”。本小节的示例是通过使用kernel32.dll去调用 Windows 的内核 API(Win32 API)调用GlobalMemoryStatusEx函数检索有关系统当前使用物理内存和虚拟内存的信息。使用到的 Win32 函数可参考https://learn.microsoft.com/zh-cn/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex关于 .NET 调用动态链接库的方式在 .NET 7 之前通过这样调用[DllImport(Kernel32.dll, CharSet CharSet.Auto, SetLastError true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern Boolean GlobalMemoryStatusEx(ref MemoryStatusExE lpBuffer);在 .NET 7 中出现了新的操作方式[LibraryImport]。文档是这样介绍的Indicates that a source generator should create a function for marshalling arguments instead of relying on the runtime to generate an equivalent marshalling function at run time. 指示源生成器应创建用于编组参数的函数而不是依赖运行库在运行时生成等效的编组函数。简单来说就是我们要使用 AOT 写代码然后代码中引用到别的动态链接库时需要使用[LibraryImport]引入这些函数。笔者没有在 AOT 下测试过[DllImport]读者感兴趣可以试试。新建两个结构体MEMORYSTATUS.cs、MemoryStatusExE.cs。MEMORYSTATUS.cspublic struct MEMORYSTATUS { internal UInt32 dwLength; internal UInt32 dwMemoryLoad; internal UInt32 dwTotalPhys; internal UInt32 dwAvailPhys; internal UInt32 dwTotalPageFile; internal UInt32 dwAvailPageFile; internal UInt32 dwTotalVirtual; internal UInt32 dwAvailVirtual; }MemoryStatusExE.cspublic struct MemoryStatusExE { /// summary /// 结构的大小以字节为单位必须在调用 GlobalMemoryStatusEx 之前设置此成员可以用 Init 方法提前处理 /// /summary /// remarks应当使用本对象提供的 Init 而不是使用构造函数/remarks internal UInt32 dwLength; /// summary /// 一个介于 0 和 100 之间的数字用于指定正在使用的物理内存的大致百分比0 表示没有内存使用100 表示内存已满。 /// /summary internal UInt32 dwMemoryLoad; /// summary /// 实际物理内存量以字节为单位 /// /summary internal UInt64 ullTotalPhys; /// summary /// 当前可用的物理内存量以字节为单位。这是可以立即重用而无需先将其内容写入磁盘的物理内存量。它是备用列表、空闲列表和零列表的大小之和 /// /summary internal UInt64 ullAvailPhys; /// summary /// 系统或当前进程的当前已提交内存限制以字节为单位以较小者为准。要获得系统范围的承诺内存限制请调用GetPerformanceInfo /// /summary internal UInt64 ullTotalPageFile; /// summary /// 当前进程可以提交的最大内存量以字节为单位。该值等于或小于系统范围的可用提交值。要计算整个系统的可承诺值调用GetPerformanceInfo核减价值CommitTotal从价值CommitLimit /// /summary internal UInt64 ullAvailPageFile; /// summary /// 调用进程的虚拟地址空间的用户模式部分的大小以字节为单位。该值取决于进程类型、处理器类型和操作系统的配置。例如对于 x86 处理器上的大多数 32 位进程此值约为 2 GB对于在启用4 GB 调整的系统上运行的具有大地址感知能力的 32 位进程约为 3 GB 。 /// /summary internal UInt64 ullTotalVirtual; /// summary /// 当前在调用进程的虚拟地址空间的用户模式部分中未保留和未提交的内存量以字节为单位 /// /summary internal UInt64 ullAvailVirtual; /// summary /// 预订的。该值始终为 0 /// /summary internal UInt64 ullAvailExtendedVirtual; internal void Refresh() { dwLength checked((UInt32)Marshal.SizeOf(typeof(MemoryStatusExE))); } }定义引用库函数的入口public static partial class Native { /// summary /// 检索有关系统当前使用物理和虚拟内存的信息 /// /summary /// param namelpBuffer/param /// returns/returns [LibraryImport(Kernel32.dll, SetLastError true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial Boolean GlobalMemoryStatusEx(ref MemoryStatusExE lpBuffer); }然后调用Kernel32.dll中的函数public class Program { static void Main() { var result GetValue(); Console.WriteLine($当前实际可用内存量{result.ullAvailPhys / 1000 / 1000}MB); Console.ReadKey(); } /// exception crefWin32Exception/exception public static MemoryStatusExE GetValue() { var memoryStatusEx new MemoryStatusExE(); // 重新初始化结构的大小 memoryStatusEx.Refresh(); // 刷新值 if (!Native.GlobalMemoryStatusEx(ref memoryStatusEx)) throw new Win32Exception(无法获得内存信息); return memoryStatusEx; } }使用 AOT 发布项目执行CsharpAot.exe文件。减少体积在前面两个例子中可以看到CsharpAot.exe文件大约在 3MB 左右但是这个文件还是太大了那么我们如何进一步减少 AOT 文件的大小呢读者可以从这里了解如何裁剪程序https://learn.microsoft.com/zh-cn/dotnet/core/deploying/trimming/trim-self-contained需要注意的是裁剪是没有那么简单的里面配置繁多有一些选项不能同时使用每个选项又能带来什么样的效果这些选项可能会让开发者用得很迷茫。经过笔者的大量测试笔者选用了以下一些配置能够达到很好的裁剪效果供读者测试。首先引入一个库ItemGroup PackageReference IncludeMicrosoft.DotNet.ILCompiler Version7.0.0-* / /ItemGroup接着在项目文件中加入以下选项!--AOT 相关-- PublishAottrue/PublishAot TrimModefull/TrimMode RunAOTCompilationTrue/RunAOTCompilation PublishTrimmedtrue/PublishTrimmed TrimmerRemoveSymbolstrue/TrimmerRemoveSymbols PublishReadyToRunEmitSymbolsfalse/PublishReadyToRunEmitSymbols DebuggerSupportfalse/DebuggerSupport EnableUnsafeUTF7Encodingtrue/EnableUnsafeUTF7Encoding InvariantGlobalizationtrue/InvariantGlobalization HttpActivityPropagationSupportfalse/HttpActivityPropagationSupport MetadataUpdaterSupporttrue/MetadataUpdaterSupport UseSystemResourceKeystrue/UseSystemResourceKeys IlcDisableReflection true/IlcDisableReflection最后发布项目。吃惊生成的可执行文件只有 1MB 了而且还可以正常执行。笔者注虽然现在看起来 AOT 的文件很小了但是如果使用到HttpClient、System.Text.Json等库哪怕只用到了一两个函数最终包含这些库以及这些库使用到的依赖生成的 AOT 文件会大得惊人。所以如果项目中使用到其他 nuget 包的时候别想着生成的 AOT 能小多少C# 导出函数这一步可以从时总的博客中学习更多跨语言调用C#代码的新方式-DllExport - InCerry - 博客园PS时总真的太强了。在 C 语言中导出一个函数的格式可以这样// MyCFuncs.h #ifdef __cplusplus extern C { // only need to export C interface if // used by C source code #endif __declspec( dllimport ) void MyCFunc(); __declspec( dllimport ) void AnotherCFunc(); #ifdef __cplusplus } #endif当代码编译之后我们就可以通过引用生成的库文件调用MyCFunc、AnotherCFunc两个方法。如果不导出的话别的程序是无法调用库文件里面的函数。因为 .NET 7 的 AOT 做了很多改进因此.NET 程序也可以导出函数了。新建一个项目名字就叫CsharpExport吧我们接下来就在这里项目中编写我们的动态链接库。添加一个CsharpExport.cs文件内容如下using System.Runtime.InteropServices; namespace CsharpExport { public class Export { [UnmanagedCallersOnly(EntryPoint Add)] public static int Add(int a, int b) { return a b; } } }然后在.csproj文件中加上PublishAot选项。然后通过以下命令发布项目生成链接库dotnet publish -p:NativeLibShared -r win-x64 -c Release看起来还是比较大为了继续裁剪体积我们可以在CsharpExport.csproj中加入以下配置以便生成更小的可执行文件。!--AOT 相关-- PublishAottrue/PublishAot TrimModefull/TrimMode RunAOTCompilationTrue/RunAOTCompilation PublishTrimmedtrue/PublishTrimmed TrimmerRemoveSymbolstrue/TrimmerRemoveSymbols PublishReadyToRunEmitSymbolsfalse/PublishReadyToRunEmitSymbols DebuggerSupportfalse/DebuggerSupport EnableUnsafeUTF7Encodingtrue/EnableUnsafeUTF7Encoding InvariantGlobalizationtrue/InvariantGlobalization HttpActivityPropagationSupportfalse/HttpActivityPropagationSupport MetadataUpdaterSupporttrue/MetadataUpdaterSupport UseSystemResourceKeystrue/UseSystemResourceKeys IlcDisableReflection true/IlcDisableReflectionC# 调用 C# 生成的 AOT在本小节中将使用CsharpAot项目调用CsharpExport生成的动态链接库。把CsharpExport.dll复制到CsharpAot项目中并配置始终复制。在CsharpAot的Native中加上[LibraryImport(CsharpExport.dll, SetLastError true)] [return: MarshalAs(UnmanagedType.I4)] internal static partial Int32 Add(Int32 a, Int32 b);然后在代码中使用static void Main() { var result Native.Add(1, 2); Console.WriteLine($1 2 {result}); Console.ReadKey(); }在 Visual Studio 里启动 Debug 调试可以看到是正常运行的。接着将CsharpAot项目发布为 AOT 后再次执行可以看到.NET AOT 调用 .NET AOT 的代码是没有问题的。Golang 部分Go 生成 Windows 动态链接库需要安装 GCC通过 GCC 编译代码生成对应平台的文件。安装 GCC需要安装 GCC 10.3如果 GCC 版本太新会导致编译 Go 代码失败。打开 tdm-gcc 官网通过此工具安装 GCC官网地址Download | tdm-gcc