如何将.NET NativeAOT构建的静态链接应用转变为完全distroless的静态链接应用?
摘要:前言 .NET NativeAOT 想必不少开发者都已经很熟悉了,它可以将 .NET 程序集直接编译到原生的机器代码,从而可以脱离 VM 直接运行。简单的一句 dotnet publish -c Release -r &lt
前言
.NET NativeAOT 想必不少开发者都已经很熟悉了,它可以将 .NET 程序集直接编译到原生的机器代码,从而可以脱离 VM 直接运行。简单的一句 dotnet publish -c Release -r <rid> /p:PublishAot=true 就可以做到。
在编写 C++ 程序之类的原生程序时,我们可能需要做静态链接,这样编译出来的程序无需在目标环境上安装使用到的库就能运行起来。这对 Linux 这种环境多变的系统非常有用。
那 .NET 的 NativeAOT 是否也做到这一点呢?
答案是:可以!
P/Invoke
在 .NET 中,想要调用原生库(.dll、.so、.dylib等等),我们常用的方法是 P/Invoke。
例如现在我有一个 C++ 库 foo.dll,导出了一个函数 int add(int x, int y),那在 .NET 中,我只需要简单的编写一句 P/Invoke 创建一个静态方法就能够调用它:
[DllImport("foo", EntryPoint = "add")]
extern static int Add(int x, int y);
Console.WriteLine(Add(3, 4));
这极大地简化了我们的工作量,我们只需要知道函数签名就能轻而易举地导入 .NET 程序中使用,甚至可以借助各种代码生成工具自动生成 P/Invoke 方法,例如 CsWin32 就是其中之一。
当调用 P/Invoke 方法时,.NET 运行时会在我们第一次调用它的时候查找并打开对应的库文件,然后获取导出符号拿到调用地址进行调用。
NativeAOT 下的 Direct P/Invoke
你会发现在 .NET 中,attribute 都是常量,而函数签名更是编译时已知的,那么 NativeAOT 下的 P/Invoke 会不会有什么编译时的针对性优化呢?
那当然是...没有的!NativeAOT 中的 P/Invoke 工作原理和非 NativeAOT 时基本上是完全一致的:也就是在运行时调用的时候才进行绑定。这么做当然是因为兼容性更好,因为即使你有一些 P/Invoke 方法在库中实际不存在,只要不去调用它也不会出现问题,因为它们都是在你调用的时候才进行绑定的。(毕竟你也不希望在 .NET 中遇到 C++ 里各种各样的构建时 unresolved symbol 链接错误)
但是!正如前面所说,NativeAOT 既然直接产生最终二进制,那么其实是可以在编译时利用到这些常量信息的。
这就是我接下来说的 Direct P/Invoke:
Direct P/Invoke 不同于 P/Invoke,它会对 P/Invoke 的方法生成直接调用,并且将函数绑定放到程序启动时由操作系统来进行。这种情况下,P/Invoke 方法会直接进入编译出的二进制的导入表,如果启动时缺失了对应的方法会直接启动失败。
使用 Direct P/Invoke 的时候,我们不需要更改任何的代码,只需要在项目文件中按照 模块名!入口点名 的格式加入需要编译成 Direct P/Invoke 的方法即可。例如我们前面 foo.dll 里面的 add,我们只需要在我们的项目文件中写:
<ItemGroup>
<DirectPInvoke Include="foo!add" />
</ItemGroup>
导入了 foo 模块中 add 函数的 P/Invoke 就全都会被自动编译成 Direct P/Invoke。
在这里入口点名甚至可以被省略,如果省略的话则表示对这个模块所有的 P/Invoke 都是 Direct P/Invoke:
<ItemGroup>
<DirectPInvoke Include="foo" />
</ItemGroup>
进一步,我们可以直接导入 libc:
<ItemGroup>
<DirectPInvoke Include="libc" />
</ItemGroup>
甚至如果列表太长的话,我们还可以单独创建一个文本文件里面一行一个,然后直接用 DirectPInvokeList 来导入:
<ItemGroup>
<DirectPInvokeList Include="NativeMethods.txt" />
</ItemGroup>
Direct P/Invoke 不仅有着更好的性能优势,而且允许我们对 P/Invoke 方法进行静态链接。
