将Go本地函数转换为C语言"回调"的方法

将Go本地函数转换为C语言"回调"的方法 我有一个C语言类型,例如 typedef int* (*callback)(int, int); 和一个C函数 void test(callback c) {c(1,2);}。 我想知道如何实现类似这样的操作:

cb := func(a,b C.int) { fmt.Println("Go callback!", a, b) }
C.test(cb)

这里是更多代码:callbacks/problem/main.go at master · gucio321/callbacks · GitHub

通常,我会使用 export,但在我的项目中必须使用局部(即lambda)函数。

我非常感谢任何帮助,因为这个问题困扰了我大约两年了,如果能最终解决它那就太好了……


6 回复

你好 @voidptr127,感谢你提供的代码。我了解这个机制,但不幸的是,在我的情况下无法使用它。我正在为一个外部项目编写一个包装器,我无法与 C.testC.callback 的类型进行交互。


这可能行得通,谢谢!

有没有可能向Go语言提交一个提案,以某种方式实现这个功能?

编辑:我说的“实现这个”是指以其他方式实现,例如为lancer添加 // export 编辑2:我说的“那可能行得通”是指使用map的代码。我看不出Dunkin MSI的代码如何解决这个问题…

我认为: 据我所知,在编译过程中,匿名函数会变成与其父函数共享环境的独立函数。也许从技术上讲,导出它们不会是什么大问题(局部环境可以像全局函数处理全局变量那样,以某种方式处理…)

补充说明:根据 CGO 维基

函数指针回调 C 代码可以通过显式名称调用导出的 Go 函数。但如果 C 程序需要一个函数指针,则必须编写一个网关函数。这是因为我们无法获取 Go 函数的地址并将其提供给 C 代码,因为 cgo 工具会生成一个应该在 C 中调用的存根。以下示例展示了如何与需要给定类型函数指针的 C 代码集成。

也许维基提供了您正在寻找的内容?

import (
    "fmt"
    "runtime/cgo"
)

/*
##include <stdint.h>

extern void go_callback_int(uintptr_t h, int p1);
static inline void CallMyFunction(uintptr_t h) {
    go_callback_int(h, 5);
}
*/
import "C"

//export go_callback_int
func go_callback_int(h C.uintptr_t, p1 C.int) {
    fn := cgo.Handle(h).Value().(func(C.int))
    fn(p1)
}

func MyCallback(x C.int) {
    fmt.Println("callback with", x)
}

func main() {
    h := cgo.NewHandle(MyCallback)
    C.CallMyFunction(C.uintptr_t(h))
    h.Delete()
}

我明白了。如果你无法更改函数定义,那么我找到的唯一解决方案是预定义一组固定数量的回调函数并进行映射。你可以编写一个脚本来生成那些样板代码,但这方法是可行的。你需要在编译时以某种方式知道匿名函数的名称才能导出它们。

示例:

package main

// extern int goCallback0(int a, int b);
// extern int goCallback1(int a, int b);
// extern int goCallback2(int a, int b);
// typedef int (*callback)(int, int);
//
// static void test(callback cb) {
//     cb(1, 2);
// }
import "C"
import (
	"fmt"
	"unsafe"
)

var cbMap map[int]func(a, b C.int) C.int
var callbackPool []unsafe.Pointer

//export goCallback0
func goCallback0(a, b C.int) C.int {
	return cbMap[0](a, b)
}

//export goCallback1
func goCallback1(a, b C.int) C.int {
	return cbMap[1](a, b)
}

//export goCallback2
func goCallback2(a, b C.int) C.int {
	return cbMap[2](a, b)
}

/* ... and so on ... */

func init() {
	cbMap = make(map[int]func(a C.int, b C.int) C.int)
	callbackPool = []unsafe.Pointer{
		C.goCallback0, C.goCallback1, C.goCallback2,
	}
}

func main() {
	cbMap[0] = func(a, b C.int) C.int {
		fmt.Println("Go callback called with", a, b)
		return a + b
	}
	defer delete(cbMap, 0)
	C.test(C.callback(callbackPool[0]))

	cbMap[1] = func(a, b C.int) C.int {
		fmt.Println("This is a private function called with", a, b)
		return a + b
	}
	defer delete(cbMap, 1)
	C.test(C.callback(callbackPool[1]))
}

回调池需要通过脚本生成到一个合理的规模(例如256、512、1024等),并且你需要处理当应用程序超过设定的可能回调数量时该怎么办。

抱歉。

让我告诉你,你正走上一条非常危险的道路,这涉及到由于CGO开销导致的性能下降、由于缺少函数引用而可能导致的崩溃,以及如果不非常小心就可能出现的内存泄漏。

有了这个免责声明,我认为如果没有某种变通方法,这是行不通的。

运行你提供的代码示例,错误如下:cannot convert myPrivCallback (variable of type func(a _Ctype_int, b _Ctype_int) _Ctype_int) to type _Ctype_callback

当你运行 C.test(C.callback(C.goCallback)) 时,你引用的是一个先前从Go世界导出的C函数。

当你尝试运行 C.test(C.callback(myPrivCallback)) 时,你引用的是一个Go函数,而不是一个导出的C函数,因此转换失败,因为这似乎是不允许的。这就是为什么你必须键入 export goCallback 来让 C.test(C.callback(C.goCallback)) 首先能够工作的原因。

话虽如此,你可以通过将你的匿名函数放在一个全局映射中并用整数来引用它们,从而绕过这个问题。在分配回调时,你必须将该整数放入C调用中,并且你还必须为此处理一个唯一的整数计数器,但这应该不是什么大问题。只需注意从映射中删除不必要的匿名函数以避免内存泄漏。

这里有一个例子:

package main

// extern int goCallback(int ci, int a, int b);
// typedef int (*callback)(int, int, int);
//
// static void test(int ci, callback cb) {
//     cb(ci, 1, 2);
// }
import "C"
import "fmt"

var cbMap map[C.int]func(a, b C.int) C.int

//export goCallback
func goCallback(ci, a, b C.int) C.int {
	return cbMap[ci](a, b)
}

func init() {
	cbMap = make(map[C.int]func(a C.int, b C.int) C.int)
}

func main() {
	cbMap[0] = func(a, b C.int) C.int {
		fmt.Println("Go callback called with", a, b)
		return a + b
	}
	defer delete(cbMap, 0)
	C.test(0, C.callback(C.goCallback))

	// Problem:
	// How to do this
	cbMap[1] = func(a, b C.int) C.int {
		fmt.Println("This is a private function called with", a, b)
		return a + b
	}
	defer delete(cbMap, 1)

	C.test(1, C.callback(C.goCallback))

	// can still call the first function because it is not deleted yet
	C.test(0, C.callback(C.goCallback))
}

我知道这不是你想要的理想解决方案。但我仍然希望这能有所帮助!

在Go中直接传递局部函数作为C回调是有限制的,但可以通过C函数指针包装器实现。以下是解决方案:

package main

/*
typedef int (*callback)(int, int);
void test(callback c) { c(1, 2); }
*/
import "C"
import (
	"fmt"
	"sync"
)

var (
	callbackMu sync.Mutex
	callbackID uintptr
	callbacks  = make(map[uintptr]func(C.int, C.int) C.int)
)

//export goCallbackBridge
func goCallbackBridge(id C.uintptr_t, a, b C.int) C.int {
	callbackMu.Lock()
	cb := callbacks[uintptr(id)]
	callbackMu.Unlock()
	
	if cb != nil {
		return cb(a, b)
	}
	return 0
}

func registerCallback(fn func(C.int, C.int) C.int) C.callback {
	callbackMu.Lock()
	defer callbackMu.Unlock()
	
	id := callbackID
	callbackID++
	callbacks[id] = fn
	
	return C.callback(C.callbackBridge(C.uintptr_t(id)))
}

func unregisterCallback(id uintptr) {
	callbackMu.Lock()
	defer callbackMu.Unlock()
	delete(callbacks, id)
}

func main() {
	// 定义局部回调函数
	cb := func(a, b C.int) C.int {
		fmt.Printf("Go callback! %d, %d\n", a, b)
		return a + b
	}
	
	// 注册并获取C回调
	cCallback := registerCallback(cb)
	defer unregisterCallback(0) // 实际使用时需要保存ID
	
	// 调用C函数
	C.test(cCallback)
}

需要配套的C桥接代码(在单独的C文件或cgo注释中):

#include <stdint.h>

extern int goCallbackBridge(uintptr_t id, int a, int b);

static int callbackBridge(uintptr_t id, int a, int b) {
    return goCallbackBridge(id, a, b);
}

如果必须完全在Go中实现,可以使用全局映射方案:

package main

/*
typedef int (*callback)(int, int);
void test(callback c) { c(1, 2); }

// 静态桥接函数
static int bridge(uintptr_t id, int a, int b);
*/
import "C"
import (
	"fmt"
	"unsafe"
)

// 使用sync.Map处理并发
var callbackStore = struct {
	sync.RWMutex
	m map[uintptr]func(C.int, C.int) C.int
}{m: make(map[uintptr]func(C.int, C.int) C.int)}

//export goCallbackHandler
func goCallbackHandler(id C.uintptr_t, a, b C.int) C.int {
	callbackStore.RLock()
	cb := callbackStore.m[uintptr(id)]
	callbackStore.RUnlock()
	
	if cb != nil {
		return cb(a, b)
	}
	return -1
}

func main() {
	// 创建局部函数
	localFunc := func(x, y C.int) C.int {
		fmt.Printf("Local callback: %d + %d = %d\n", x, y, x+y)
		return x * y
	}
	
	// 生成唯一ID并存储
	funcID := uintptr(unsafe.Pointer(&localFunc))
	callbackStore.Lock()
	callbackStore.m[funcID] = localFunc
	callbackStore.Unlock()
	
	// 清理函数
	defer func() {
		callbackStore.Lock()
		delete(callbackStore.m, funcID)
		callbackStore.Unlock()
	}()
	
	// 调用C函数
	C.test(C.callback(unsafe.Pointer(C.bridge(C.uintptr_t(funcID)))))
}

C桥接部分:

#include <stdint.h>

extern int goCallbackHandler(uintptr_t id, int a, int b);

static int bridge(uintptr_t id, int a, int b) {
    return goCallbackHandler(id, a, b);
}

关键点:

  1. 使用C桥接函数作为静态中间层
  2. 通过ID映射Go回调函数
  3. 处理内存管理和并发安全
  4. 局部函数通过闭包捕获上下文,但C只能调用静态函数

这种方法允许在Go中使用局部函数作为C回调,同时保持类型安全和内存安全。

回到顶部