将Go指针数组传递给C的Golang实现方法

将Go指针数组传递给C的Golang实现方法 根据 cgo 规则 § 传递指针:

Go 代码可以将 Go 指针传递给 C,前提是该指针所指向的 Go 内存不包含任何 Go 指针。

因此,像下面这样的代码是不允许的:

func test_slices(slices ...[]byte) int {
	lens := make([]C.size_t, len(slices))
	ptrs := make([]*C.uchar, len(slices))
	for i, _ := range slices {
		lens[i] = (C.size_t)(len(slices[i]))
		ptrs[i] = (*C.uchar)(&slices[i][0])
	}
	cPtrInputs := (**C.uchar)(&ptrs[0])
	cSizesPtr := (*C.size_t)(&lens[0])
	cAmounts := (C.size_t)(len(slices))
	C.test_slices(cPtrInputs, cSizesPtr, cAmounts)
}

这是为什么呢?为什么我们可以将 Go 指针传递给 C,却不能将一个 Go 指针数组传递给 C?

另外,正确的做法是什么?通过 C.malloc 分配 C 指针 + 复制 + 传递这些指针吗?有没有一种方法可以避免 malloc + copy 的开销?


更多关于将Go指针数组传递给C的Golang实现方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我认为来自 cgoCheckPointer 的这段源代码暗示了原因:

// When and if we implement a moving garbage collector,
// cgoCheckPointer will pin the pointer for the duration of the cgo
// call.  (This is necessary but not sufficient; the cgo program will
// also have to change to pin Go pointers that cannot point to Go
// pointers.)

如果允许传递指向指针的指针,那么当/如果 Go 开始移动其内存分配时,尽管切片本身被固定了,但切片内的任何指针元素都可能在任何时候被移动,cgo 最终可能会访问到已释放/已被重新利用的内存。为了防止这种情况,cgoCheckPointer 将不得不做一些事情,比如递归地固定整个指针到指针到指针……的图,对于使用 cgo 的程序完全禁用内存移动,或许在 cgo 调用期间禁用移动,等等。

更多关于将Go指针数组传递给C的Golang实现方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据 cgo 规则,传递包含 Go 指针的指针数组确实违反规则。原因在于 C 代码可能存储这些指针并在后续 Go 内存管理(如垃圾回收)期间访问它们,而 Go 运行时无法追踪 C 中存储的 Go 指针。

以下是正确的实现方法:

package main

/*
#include <stdlib.h>
void process_slices(unsigned char** ptrs, size_t* lens, size_t count) {
    // C 函数处理切片数据
}
*/
import "C"
import (
	"unsafe"
)

func processGoSlices(slices [][]byte) {
	if len(slices) == 0 {
		return
	}

	// 分配 C 内存存储指针和长度
	cPtrs := (**C.uchar)(C.malloc(C.size_t(len(slices)) * C.size_t(unsafe.Sizeof(uintptr(0)))))
	cLens := (*C.size_t)(C.malloc(C.size_t(len(slices)) * C.size_t(unsafe.Sizeof(C.size_t(0)))))

	// 获取底层数组指针
	ptrSlice := (*[1 << 30]*C.uchar)(unsafe.Pointer(cPtrs))[:len(slices):len(slices)]
	lenSlice := (*[1 << 30]C.size_t)(unsafe.Pointer(cLens))[:len(slices):len(slices)]

	// 填充数据
	for i := range slices {
		if len(slices[i]) > 0 {
			ptrSlice[i] = (*C.uchar)(unsafe.Pointer(&slices[i][0]))
		} else {
			ptrSlice[i] = nil
		}
		lenSlice[i] = C.size_t(len(slices[i]))
	}

	// 调用 C 函数
	C.process_slices(cPtrs, cLens, C.size_t(len(slices)))

	// 释放 C 内存
	C.free(unsafe.Pointer(cPtrs))
	C.free(unsafe.Pointer(cLens))
}

如果希望避免复制数据,可以使用以下方法直接传递 Go 内存:

func processSlicesNoCopy(slices [][]byte) {
	if len(slices) == 0 {
		return
	}

	// 创建指向第一个元素的指针
	ptrArray := make([]*C.uchar, len(slices))
	for i := range slices {
		if len(slices[i]) > 0 {
			ptrArray[i] = (*C.uchar)(unsafe.Pointer(&slices[i][0]))
		} else {
			ptrArray[i] = nil
		}
	}

	// 通过 unsafe 转换为 C 指针
	cPtrs := (**C.uchar)(unsafe.Pointer(&ptrArray[0]))
	cLens := make([]C.size_t, len(slices))
	for i := range slices {
		cLens[i] = C.size_t(len(slices[i]))
	}

	// 调用时确保 C 函数不会存储指针
	runtime.KeepAlive(slices) // 防止垃圾回收
	runtime.KeepAlive(ptrArray)
	C.process_slices(cPtrs, &cLens[0], C.size_t(len(slices)))
}

关键点:

  1. C 代码必须同步使用指针,不能存储供后续使用
  2. 需要确保 Go 切片在 C 调用期间保持活跃
  3. 空切片需要特殊处理

对于性能敏感场景,建议使用第一种 C.malloc 方法,虽然需要额外分配,但完全符合 cgo 规则且安全。

回到顶部