首页 > 解决方案 > 如何“将 Go 指针传递给 Cgo”?

问题描述

我对将 Go 指针(据我了解,包括所有指针类型以及unsafe.Pointer)传递给 cgo 感到困惑。当使用 cgo 调用 C 函数时,我只能提供 C 端已知类型的变量,或者unsafe.Pointer如果它与void*C 函数签名中的 -typed 参数匹配。因此,当“传递给 C 的 Go 指针在调用的生命周期内被固定”C.some_wide_enough_uint_type时,如果我被迫将其强制转换为或C.some_c_pointer_type预先转换,Go 怎么知道我传递的实际上是 Go 指针?在它被投射的那一刻,它是一个 Go 指针的信息不是丢失了,我冒着 GC 改变指针的风险吗?(我可以看到至少在 Go 端保留指针类型引用时如何防止释放)

我们有一个项目有相当多的工作 cgo 代码,但对其可靠性的信心为零。我想看一个“这里是如何正确地做到这一点”的例子,它不会通过使用C.malloc()或诸如此类来规避 Go 的内存模型,不幸的是大多数例子都是这样做的。

因此,无论“固定调用生命周期的指针”实际上意味着什么,我都看到了一个问题:

  1. 如果这意味着 Go 将固定整个程序中的所有指针,我会在将 Go 指针转换为 C 类型和实际调用 cgo 调用之间的时间间隔中看到竞争条件。
  2. 如果这意味着 Go 将仅固定那些正在传递的 Go 指针,那么当调用时它们只能具有 C 类型时,它如何知道它们是 Go 指针?

我已经阅读了半天的 Go 问题,并且开始觉得我只是错过了一些简单的东西。任何指针表示赞赏。

编辑:我将尝试通过提供示例来澄清这个问题。

考虑一下:

/*
#include <stdio.h>
void myCFunc(void* ptr) {
    printf((char*)ptr);
}
*/
import "C"
import "unsafe"

func callMyCFunc() {
    goPointer := []byte("abc123\n\x00")
    C.myCFunc(unsafe.Pointer(&goPointer[0]))
}

在这里,Go's unsafe.Pointer-type 毫不费力地转换为 C's void*-type,所以我们在 C 方面很高兴,我们也应该在 Go 方面:指针清楚地指向 Go 分配的内存,所以它应该是微不足道的让 Go 弄清楚它应该在调用期间固定这个指针,尽管它是一个不安全的指针。是这样吗?如果是这样,无需进一步研究,我会认为这是将 Go 指针传递给 cgo 的首选方式。是吗?

然后,考虑一下:

/*
#include <stdio.h>
void myCFunc(unsigned long long int stupidlyTypedPointerVariable) {
    char* pointerToHopefullyStillTheSameMemory = (char*)stupidlyTypedPointerVariable;
    printf(pointerToHopefullyStillTheSameMemory);
}
*/
import "C"
import "unsafe"

func callMyCFunc() {
    goPointer := []byte("abc123\n\x00")
    C.myCFunc(C.ulonglong(uintptr(unsafe.Pointer(&goPointer[0]))))
}

在这里,我希望 Go 不会对某个C.ulonglong-typed 变量是否实际上意味着包含 Go 指针的地址做出任何猜测。但我是对的吗?

我的困惑很大程度上源于这样一个事实,即实际上不可能编写一些代码来可靠地测试它。

最后,这个呢:

/*
#include <stdio.h>
void cFuncOverWhichIHaveNoControl(char* ptr) {
    printf(ptr);
}
*/
import "C"
import "unsafe"

func callMyCFunc() {
    goPointer := []byte("abc123\n\x00")
    C.cFuncOverWhichIHaveNoControl((*C.char)(unsafe.Pointer(&goPointer[0])))
}

如果我出于某种原因无法更改 C 函数的签名,我必须强制转换为*C.char. 当值已经是 C 指针类型时,Go 还会检查值是否是 Go 指针吗?

标签: go

解决方案


查看当前 cgo 文档中关于传递指针的部分,(感谢 peterSO)我们发现

Go 指针一词是指指向 Go 分配的内存的指针

以及该

指针类型可以保存 Go 指针或 C 指针

因此,使用uintptr和其他整数(读取:非指针)类型将失去 Go 对固定指针的保证。

uintptr 是整数,而不是引用。将指针转换为 uintptr 会创建一个没有指针语义的整数值。即使 uintptr 拥有某个对象的地址,如果对象移动,垃圾收集器也不会更新该 uintptr 的值,该 uintptr 也不会阻止对象被回收。

来源:https ://golang.org/pkg/unsafe/#Pointer

关于诸如*char/之类的 C 指针类型*C.char,只有当指向的数据本身不包含指向 Go 分配的其他内存的指针时,它们才是安全的。这实际上可以通过尝试触发 Go 的 Cgo 调试机制来展示,该机制不允许将 Go 指针传递给(或传入)本身包含另一个 Go 指针的值:

package main

import (
    "fmt"
    "unsafe"

    /*
    #include <stdio.h>

    void cFuncChar(char* ptr) {
        printf("%s\n", ptr);
    }

    void cFuncVoid(void* ptr) {
        printf("%s\n", (char*)ptr);
    }
    */
    "C"
)

type MyStruct struct {
    Distraction [2]byte
    Dangerous *MyStruct
}

func main() {
    bypassDetection()
    triggerDetection()
}

func bypassDetection() {
    fmt.Println("=== Bypass Detection ===")
    ms := &MyStruct{[2]byte{'A', 0}, &MyStruct{[2]byte{0, 0}, nil}}
    C.cFuncChar((*C.char)(unsafe.Pointer(ms)))
}

func triggerDetection() {
    fmt.Println("=== Trigger Detection ===")
    ms := &MyStruct{[2]byte{'B', 0}, &MyStruct{[2]byte{0, 0}, nil}}
    C.cFuncVoid(unsafe.Pointer(ms))
}

这将打印以下内容:

=== Bypass Detection ===
A
=== Trigger Detection ===
panic: runtime error: cgo argument has Go pointer to Go pointer

使用*C.char绕过了检测。只有使用unsafe.Pointer才会检测 Go 指针到 Go 指针的场景。不幸的是,这意味着我们将不得不void*在 C 函数的签名中偶尔添加一个模糊的 - 参数。

为清楚起见添加:Go 可以很好地固定 a*C.char或此类所指向的值,这是可以安全通过的;它只是(合理地)不会努力找出它是否可能包含其他指向 Go 分配的内存的指针。投射到unsafe.Pointer实际上是安全的;从中铸造可能是危险的。


推荐阅读