Golang CShared模式下委托函数调用崩溃问题求助
回调崩溃核心原因
Go 编译为 c-shared 库时,非C兼容类型会被自动转为 void*,若调用方(Dart/C#)未正确做类型映射、或Go侧未规范参数传递,会直接引发内存访问错误导致崩溃。
分步修复方案
1. Go侧规范回调类型与参数传递
必须用C兼容类型定义回调签名,禁止直接使用Go原生类型(如string),示例代码:
package main /* typedef void (*CallbackFunc)(const char*); */ import "C" import "unsafe" var callback C.CallbackFunc //export SetCallback func SetCallback(cb C.CallbackFunc) { callback = cb } //export TriggerCallback func TriggerCallback() { if callback != nil { // 转换Go字符串为C字符串,需手动释放内存 cMsg := C.CString("callback from Go") defer C.free(unsafe.Pointer(cMsg)) callback(cMsg) } } func main() {} // 必须保留空main函数用于编译
编译命令:
go build -buildmode=c-shared -o libcallback.so libcallback.go
2. Dart/ffigen 侧正确绑定与类型转换
在pubspec.yaml中配置ffigen的类型映射,将void*转为对应C类型:
ffigen: output: 'generated_bindings.dart' headers: entry-points: - 'libcallback.h' type-map: char*: Pointer<Char> void*: Pointer<Void>
调用时需将Dart函数转为符合C调用约定的指针:
import 'dart:ffi'; import 'generated_bindings.dart'; import 'package:ffi/ffi.dart'; void main() { final dylib = DynamicLibrary.open('libcallback.so'); final lib = Libcallback(dylib); // 定义回调逻辑,正确解析C字符串 void callbackHandler(Pointer<Char> msg) { print("Received: ${msg.toDartString()}"); } // 转换为Go兼容的函数指针 final callbackPtr = Pointer.fromFunction<Void Function(Pointer<Char>)>(callbackHandler); lib.SetCallback(callbackPtr); lib.TriggerCallback(); }
3. C# 侧匹配调用约定与参数解析
C#需显式指定cdecl调用约定(Go默认调用约定),并正确转换C指针为字符串:
using System; using System.Runtime.InteropServices; class Program { // 匹配Go侧的回调签名与调用约定 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void CallbackFunc(IntPtr msg); [DllImport("libcallback.so", CallingConvention = CallingConvention.Cdecl)] private static extern void SetCallback(CallbackFunc cb); [DllImport("libcallback.so", CallingConvention = CallingConvention.Cdecl)] private static extern void TriggerCallback(); static void Main(string[] args) { CallbackFunc callback = (IntPtr msgPtr) => { string msg = Marshal.PtrToStringAnsi(msgPtr); Console.WriteLine($"Received: {msg}"); }; SetCallback(callback); TriggerCallback(); } }
Go与C交互的类型处理规范
字符串处理
- Go → C:使用
C.CString(goStr)将Go字符串转为C字符串,内存分配在C堆,必须用C.free(unsafe.Pointer(cStr))释放,避免泄漏。 - C → Go:使用
C.GoString(cStr)将C字符串转为Go字符串,会自动复制数据到Go内存,无需手动释放C侧内存(若C内存由Go分配仍需free)。
字节数组处理
- Go切片 → C数组:先分配C内存,再复制数据:
func GoSliceToC(data []byte) (*C.uchar, C.int) { cBuf := (*C.uchar)(C.malloc(C.size_t(len(data)))) copy((*[1 << 30]byte)(unsafe.Pointer(cBuf))[:len(data)], data) return cBuf, C.int(len(data)) }
- C数组 → Go切片:基于C指针创建Go切片,复制数据:
func CArrayToGo(cBuf *C.uchar, length C.int) []byte { goSlice := make([]byte, length) copy(goSlice, (*[1 << 30]byte)(unsafe.Pointer(cBuf))[:length]) return goSlice }
关键注意事项
- 明确内存所有权:Go分配的C内存(
C.CString/C.malloc)需由Go或调用方释放;调用方分配的内存由调用方负责。 - 统一调用约定:Go默认使用
cdecl,所有调用方必须匹配该约定,否则会引发栈错误。 - 回调中避免复杂Go操作:禁止在回调内启动goroutine或调用Go runtime的复杂API,防止线程安全问题。
内容的提问来源于stack exchange,提问作者Ahmad Asy Syafiq
相关产品推荐
相关产品推荐

