C#中同名方法导出与导入的native AOT异常排查
在Windows平台使用.NET 7的Native AOT时,遇到异常行为:当导出的方法与从其他原生库导入的方法同名时,调用导入的方法会触发自身导出的方法。
原本想要创建一个原生库,暴露CreateFileW方法并转发给kernel32.dll中的真实CreateFileW方法,于是创建启用Native AOT的类库并发布,代码如下:
[DllImport("kernel32.dll", EntryPoint = "CreateFileW")] public static extern IntPtr RealCreateFile( IntPtr filename, uint access, uint share, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile); [UnmanagedCallersOnly(EntryPoint = "CreateFileW")] public static IntPtr MyCreateFile( IntPtr lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { Console.Out.WriteLine("Calling CreateFileW"); return RealCreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }
随后创建控制台应用调用该库,代码如下:
[DllImport("NativeClassTest.dll", EntryPoint = "CreateFileW")] public static extern IntPtr MyCreateFile( string filename, uint access, uint share, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile); static void Main(string[] args) { var handle = MyCreateFile(@"C:\temp\test.txt", 1, 0, IntPtr.Zero, 3, 128, IntPtr.Zero); }
运行应用后会反复打印Calling CreateFileW直至栈溢出,但修改导出方法名称后一切正常。排查过封送问题但不符,也无法调试AOT编译的库。
请问这是已知或预期行为吗?DllImport的工作机制是什么,是否使用错误?
解答
这是Native AOT编译时符号解析优先级导致的预期行为。在Windows的PE文件中,当你用DllImport导入外部函数时,Native AOT编译器会将导入的符号与当前模块(你的Native AOT库)中的导出符号进行匹配——如果当前模块已经导出了同名符号,编译器会直接绑定到自身的导出方法,而不会去加载外部kernel32.dll中的同名函数,最终导致递归调用。
DllImport的工作机制(Native AOT下)
在标准.NET(非AOT)中,DllImport的解析是运行时完成的,CLR会根据指定的DLL名称加载对应库并查找符号。但在Native AOT中,符号解析是编译时静态绑定的:
- 编译器会收集所有
DllImport声明,尝试在编译阶段解析符号地址。 - 如果当前模块(你的库)已经导出了同名符号,编译器会优先使用本地导出的符号,而忽略
DllImport指定的外部DLL。
解决方法
要避免这种递归,需要明确告诉Native AOT编译器,RealCreateFile必须绑定到kernel32.dll的CreateFileW,而不是自身的导出符号。可以通过以下方式实现:
1. 为DllImport添加ExactSpelling = true
指定ExactSpelling = true后,编译器会严格按照指定的DLL和入口点查找符号,不会优先匹配本地导出的同名符号:
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", ExactSpelling = true)] public static extern IntPtr RealCreateFile( IntPtr filename, uint access, uint share, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile);
2. 配合[SuppressGCTransition]强化绑定(可选)
如果上述方法效果不佳,可以添加SuppressGCTransition属性,在减少托管/非托管切换开销的同时,强制编译器严格解析外部符号:
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", ExactSpelling = true)] [SuppressGCTransition] public static extern IntPtr RealCreateFile( IntPtr filename, uint access, uint share, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile);
验证
修改后重新编译Native AOT库,控制台应用调用时,RealCreateFile会正确调用kernel32.dll的原生方法,不会触发自身导出的CreateFileW,递归栈溢出问题即可解决。
内容的提问来源于stack exchange,提问作者Kirie

