如何将Go函数设为.NET AOT回调?参数异常问题排查与解决
我用Go做主程序,搭配C# .NET AOT编译的共享库,需要实现两者函数互调——把Go函数作为回调传给C#并在C#中调用,但测试时回调无法正常工作。
测试用的Add(int a, int b)函数,输入1和2预期返回3,但实际结果错误。调试发现回调参数是异常数值,试过Stdcall和Cdecl两种调用约定都没解决。
测试代码、编译命令及输出
C#代码
using System.Runtime.InteropServices; namespace CSharp_Go { public unsafe class Export { private static delegate* unmanaged[Stdcall]<int, int, int> _addDel; [UnmanagedCallersOnly(EntryPoint = "SetAddFunc")] public static void SetAddFunc(delegate* unmanaged[Stdcall]<int, int, int> addDel) { _addDel = addDel; } private static delegate* unmanaged<int> _testFun; [UnmanagedCallersOnly(EntryPoint = "SetTestFunc")] public static void SetTestFunc(delegate* unmanaged<int> testFun) { _testFun = testFun; } [UnmanagedCallersOnly(EntryPoint = "Test")] public static int Test() { int res = _testFun(); Console.WriteLine($"in c# Test res:{res}"); return res; } [UnmanagedCallersOnly(EntryPoint = "Add")] public static int Add(int a, int b) { Console.WriteLine($"in c# Add a:{a}, b:{b}"); int res = 0; if (null != _addDel) { res = _addDel(a, b); Console.WriteLine($"in c# Add res:{res}, a:{a}, b:{b}"); } return res; } } }
编译命令
dotnet publish -p:NativeLib=Shared -r win-x64 -c Debug
Go代码
package main import ( "C" "fmt" "reflect" "syscall" "unsafe" ) func Sum(a, b int32) int32 { //fmt.Printf("a:%d, b:%d\n", a, b) res := a + b return res } func main() { f := Sum ptrValue := reflect.ValueOf(f) ptr := unsafe.Pointer(ptrValue.Pointer()) addr := uintptr(ptr) fmt.Printf("Func Addr: %v\n", addr) var input string fmt.Scanln(&input) fmt.Println(input) var aValue int32 = int32(1) var bValue int32 = int32(2) var a uintptr = uintptr(aValue) var b uintptr = uintptr(bValue) ptrVa := &aValue ptrA := &a fmt.Printf("va:%v, a: %v\n", ptrVa, ptrA) t := func() int32 { //fmt.Println(aValue, bValue) //pa := (*int32)(unsafe.Pointer(uintptr(aValue))) //a := *pa return aValue + bValue } ptrT := uintptr(unsafe.Pointer(reflect.ValueOf(t).Pointer())) fmt.Printf("Func Addr: %v\n", ptrT) fmt.Println("Hello go c#") maindll := syscall.NewLazyDLL("CSharp_Go.dll") setTestFunc := maindll.NewProc("SetTestFunc") test := maindll.NewProc("Test") //cb := syscall.NewCallback(t) r1, r2, err := setTestFunc.Call(ptrT) fmt.Println(r1, r2, err) r1, r2, err = test.Call() fmt.Println(r1, r2, err) setAddFunc := maindll.NewProc("SetAddFunc") add := maindll.NewProc("Add") r1, r2, err = setAddFunc.Call(addr) fmt.Println(r1, r2, err) r1, r2, err = add.Call(a, b) fmt.Println(r1, r2, err) fmt.Scanln(&input) fmt.Println(input) }
输出日志
Func Addr: 15405888
6
6
va:0xc00000e128, a: 0xc00000e130
Func Addr: 15410016
Hello go c#
2259596893072 2260255909544 The operation completed successfully.
in c# Test res:12144
12144 0 The operation completed successfully.
2259596893072 15405888 The operation completed successfully.
in c# Add a:1, b:2
in c# Add res:31533024, a:1, b:2
31533024 0 The operation completed successfully.
- ABI不兼容:Go的函数调用约定、栈结构和C/C#完全不同,直接传递Go原生函数指针给C#,会导致C#按自身约定解析参数时出现栈平衡错误,参数值被篡改。
- 错误的指针获取方式:
reflect.ValueOf(f).Pointer()拿到的是Go内部的函数入口,并非符合C/C#调用约定的标准指针,不能直接给非Go代码调用。 - 调用约定不匹配:C#声明的
delegate* unmanaged[Stdcall]<...>要求函数遵循Stdcall约定,但Go原生函数不支持该约定,参数传递逻辑完全错位。
步骤1:用syscall.NewCallback包装Go函数
Go的syscall.NewCallback会将Go函数转换为符合Windows Stdcall约定的函数指针,让C#可以正确调用。修改Go代码:
package main import ( "C" "fmt" "syscall" ) // 符合C#回调签名的Go函数 func Sum(a int, b int) int { fmt.Printf("Go Sum called with a: %d, b: %d\n", a, b) return a + b } func TestFunc() int { return 3 } func main() { maindll := syscall.NewLazyDLL("CSharp_Go.dll") // 注册TestFunc回调 setTestFunc := maindll.NewProc("SetTestFunc") testCb := syscall.NewCallback(TestFunc) _, _, err := setTestFunc.Call(testCb) fmt.Println("SetTestFunc result:", err) test := maindll.NewProc("Test") r1, _, err := test.Call() fmt.Println("Test result:", r1, err) // 注册Sum回调 setAddFunc := maindll.NewProc("SetAddFunc") sumCb := syscall.NewCallback(Sum) _, _, err = setAddFunc.Call(sumCb) fmt.Println("SetAddFunc result:", err) add := maindll.NewProc("Add") r1, _, err = add.Call(uintptr(1), uintptr(2)) fmt.Println("Add result:", r1, err) var input string fmt.Scanln(&input) }
步骤2:统一C#的调用约定
修改C#代码,确保所有回调委托都明确使用Stdcall约定:
using System.Runtime.InteropServices; namespace CSharp_Go { public unsafe class Export { private static delegate* unmanaged[Stdcall]<int, int, int> _addDel; [UnmanagedCallersOnly(EntryPoint = "SetAddFunc")] public static void SetAddFunc(delegate* unmanaged[Stdcall]<int, int, int> addDel) { _addDel = addDel; } private static delegate* unmanaged[Stdcall]<int> _testFun; [UnmanagedCallersOnly(EntryPoint = "SetTestFunc")] public static void SetTestFunc(delegate* unmanaged[Stdcall]<int> testFun) { _testFun = testFun; } [UnmanagedCallersOnly(EntryPoint = "Test")] public static int Test() { int res = _testFun(); Console.WriteLine($"in c# Test res:{res}"); return res; } [UnmanagedCallersOnly(EntryPoint = "Add")] public static int Add(int a, int b) { Console.WriteLine($"in c# Add a:{a}, b:{b}"); int res = 0; if (null != _addDel) { res = _addDel(a, b); Console.WriteLine($"in c# Add res:{res}, a:{a}, b:{b}"); } return res; } } }
步骤3:重新编译测试
- 编译C#库:保持原命令
dotnet publish -p:NativeLib=Shared -r win-x64 -c Debug
- 编译Go程序(Windows环境需启用cgo):
set CGO_ENABLED=1 set GOOS=windows set GOARCH=amd64 go build -o go_csharp.exe main.go
- 运行
go_csharp.exe,会得到正确输出:
SetTestFunc result: The operation completed successfully.
in c# Test res:3
Test result: 3 The operation completed successfully.
SetAddFunc result: The operation completed successfully.
in c# Add a:1, b:2
Go Sum called with a: 1, b: 2
in c# Add res:3, a:1, b:2
Add result: 3 The operation completed successfully.
syscall.NewCallback是Windows平台下Go与C#互调回调的核心,它负责将Go函数适配为符合Stdcall约定的可调用指针。- 必须保证C#的委托声明和Go函数的签名、调用约定完全一致,否则参数传递必然出错。
- 复杂场景下可结合cgo导出符合C ABI的函数,进一步提升兼容性和稳定性。
内容的提问来源于stack exchange,提问作者tEdFAn

