.NET 7 AOT 的使用方法
背景
其实,规划这篇文章有一段时间了,但是比较懒,所以一直拖着没写。
最近时总更新太快了,太卷了,所以借着 .NET 7 正式版发布,熬夜写完这篇文章,希望能够追上时总的一点距离。
本文主要介绍如何在 .NET 和 Go 语言中如何生成系统(Windows)动态链接库,又如何从代码中引用这些库中的函数。
在 .NET 部分,介绍如何使用 AOT、减少二进制文件大小、使用最新的 [LibraryImport]
导入库函数;
在 Go 语言部分,介绍如何使用 GCC 编译 Go 代码、如何通过 syscall
导入库函数。
在文章中会演示 .NET 和 Go 相互调用各自生成的动态链接库,以及对比两者之间的差异。
本文文章内容以及源代码,可以 https://github.com/whuanle/csharp_aot_golang 中找到,如果本文可以给你带来帮助,可以到 Github 点个星星嘛。
C# 部分
环境要求
SDK:.NET 7 SDK、Desktop development with C++ workload。
IDE:Visual Studio 2022
Desktop 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 模式,需要在项目文件中加上 <PublishAot>true</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 之前,通过这样调用:
[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.cs
:
public 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.cs
:
public 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 name="lpBuffer"></param>
/// <returns></returns>
[ ]
[ ] 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 cref="Win32Exception"></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 Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>
接着,在项目文件中加入以下选项:
<!--AOT 相关-->
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<RunAOTCompilation>True</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<PublishReadyToRunEmitSymbols>false</PublishReadyToRunEmitSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeUTF7Encoding>true</EnableUnsafeUTF7Encoding>
<InvariantGlobalization>true</InvariantGlobalization>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>true</MetadataUpdaterSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<IlcDisableReflection >true</IlcDisableReflection>
最后,发布项目。
吃惊!生成的可执行文件只有 1MB 了,而且还可以正常执行。

笔者注:虽然现在看起来 AOT 的文件很小了,但是如果使用到 HttpClient
、System.Text.Json
等库,哪怕只用到了一两个函数,最终包含这些库以及这些库使用到的依赖,生成的 AOT 文件会大得惊人。
所以,如果项目中使用到其他 nuget 包的时候,别想着生成的 AOT 能小多少!
C# 导出函数
这一步可以从时总的博客中学习更多:https://www.cnblogs.com/InCerry/p/CSharp-Dll-Export.html
PS:时总真的太强了。

在 C 语言中,导出一个函数的格式可以这样:
// MyCFuncs.hextern "C" { // only need to export C interface if
// used by C++ source code __declspec( dllimport ) void MyCFunc();
__declspec( dllimport ) void AnotherCFunc(); }
当代码编译之后,我们就可以通过引用生成的库文件,调用 MyCFunc
、AnotherCFunc
两个方法。
如果不导出的话,别的程序是无法调用库文件里面的函数。
因为 .NET 7 的 AOT 做了很多改进,因此,.NET 程序也可以导出函数了。
新建一个项目,名字就叫 CsharpExport
吧,我们接下来就在这里项目中编写我们的动态链接库。
添加一个 CsharpExport.cs
文件,内容如下:
using System.Runtime.InteropServices;namespace CsharpExport{ public class Export
{
[ ] public static int Add(int a, int b)
{ return a + b;
}
}
}
然后在 .csproj
文件中,加上 PublishAot
选项。

然后通过以下命令发布项目,生成链接库:
dotnet publish -p:NativeLib=Shared -r win-x64 -c Release

看起来还是比较大,为了继续裁剪体积,我们可以在 CsharpExport.csproj
中加入以下配置,以便生成更小的可执行文件。
<!--AOT 相关-->
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<RunAOTCompilation>True</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<PublishReadyToRunEmitSymbols>false</PublishReadyToRunEmitSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeUTF7Encoding>true</EnableUnsafeUTF7Encoding>
<InvariantGlobalization>true</InvariantGlobalization>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>true</MetadataUpdaterSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<IlcDisableReflection >true</IlcDisableReflection>

C# 调用 C# 生成的 AOT
在本小节中,将使用 CsharpAot
项目调用 CsharpExport
生成的动态链接库。
把 CsharpExport.dll
复制到 CsharpAot
项目中,并配置 始终复制。

在 CsharpAot
的 Native
中加上:
[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 的代码是没有问题的。