从 Python ctypes 调用 Go 字符串函数导致段错误

Calling a Go string function from Python ctypes results in segfault

我有一个名为 test.go 的模块,其中包含两个接受字符串类型的简单 Go 函数:

package main

import (
  "fmt"
  "C"
)

//export TestConcat
func TestConcat(testArg string, testArg2 string) (string) {
  retval := testArg + testArg2
  return retval
}

//export TestHello
func TestHello(testArg string) {
  fmt.Println("%v\n", testArg)
}


func main(){}

我用go build -o test.so -buildmode=c-shared test.go

将它编译为共享库

然后我有一个名为 test.py

的 Python 模块
import ctypes

from ctypes import cdll


test_strings = [
    "teststring1",
    "teststring2"
]

if __name__ == '__main__':
    lib = cdll.LoadLibrary("./test.so")
    lib.TestConcat.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p]
    lib.TestHello.argtypes = [ctypes.c_wchar_p]
    for test_string in test_strings:
        print(
            lib.TestConcat("hello", test_string)
        )
        lib.TestHello(test_string)

然后我 运行 test.py 得到一个讨厌的段错误

runtime: out of memory: cannot allocate 279362762964992-byte block (66781184 in use)
fatal error: out of memory

我试过将参数包装在 ctypes.c_wchar_p 中但无济于事。

我在这里做错了什么?具体来说,如何与接受字符串参数的 Go 函数交互 Python?

Go 的 string 类型实际上类似于

type string {
    ptr *byte
    size int
}

所以这就是 Test{Hello|Concat} 实际期望的——不是一对指针,而是一对 struct 类型的值。
换句话说,cgo 对从 Go 到 C 和返回的网关调用执行了足够的魔法,但它不执行值的自动转换。

你有两个选择:

  • 如果可能,请从您的 ctypes 绑定中明确使用它。
    编译你的包时,cgo 生成一个头文件,其中包含表示 Go 字符串的结构的 C 定义;你可以马上使用它。

  • 使导出到 C 的函数与 C 的“类型系统”兼容。
    为此,cgo 提供 helper functions C.CString and C.GoString.
    基本上,您可以这样定义 API:

    func TestHello(a, b *C.char) *C.char {
        testArg1, testArg2 := C.GoString(a), C.GoString(b)
        return C.CString(testArg + TestArg2)
    }
    

    注意这里的一些注意事项:

    • 这两个助手都复制了他们参数的内存,所以上面那个愚蠢的例子可以正常工作,但它会首先复制 ab 指向的内存块,然后吃增加两倍的内存来生成连接的字符串,然后再次复制结果字符串的内存以生成返回的指针。
      IOW,如果你试图将一些大块的 Go 代码导出到 C 中,那么这种方法很好,这样这些分配与该块所做的任何事情都相形见绌。
    • 使用*C.char等同于C语言中的*char,所以字符串应该以NUL结尾;如果不是,请使用 C.GoStringN.
    • 每个由 C.CString 分配的内存块都必须通过调用 C.free 来释放。这里有一个转折:C.free 基本上是一个从 libc 中的链接调用 free() 的薄垫片,所以如果你能保证完整的产品(代码完全加载到内存中并且(inter )使用动态链接器链接)只有一个链接的 libc 副本,您可以从调用 C.Cstring 中产生的内存块上的非 Go 代码调用 free()去代码。

另外几个随机指针:

  • 我不精通 Python 的 ctypes 但我推测使用 ctypes.c_wchar_p 是不正确的:在 C(和 C++,FWIW)中 wchar_t 是一种表示单个 fixed-sized “宽字符”的类型,通常是 一个 UCS-2/UTF-16 code point, and Go's strings are not composed of these—they may contain arbitrary bytes, and when they are used to contain Unicode text, they are encoded using UTF-8 这是一个多字节编码(单个 Unicode 代码点可能由字符串中的 1 到 4 个字节表示)。
    在任何一种情况下,wchar_t 都不能用于 UTF-8(实际上是 many seasoned devs beleive it's an abomination)。
  • 在开始这个项目之前,请阅读 the docs on cmd/cgo 。真的,请做!