文章目录
- go编写python模块(go 语言开发 Python 扩展)
- 1. 什么是python拓展模块
- 2. go 语言开发 Python 扩展思路
- 3. go编写python2拓展模块 示例
- 4. go编写python3拓展模块 示例
- 代码优化
- 5. python拓展模块什么时候加载
- 查找拓展模块so文件位置的顺序
- 6. go和c数据结构转化对应关系
- 7. python C 拓展模块常用api
- 8. python调用so go函数原理
go编写python模块(go 语言开发 Python 扩展)
1. 什么是python拓展模块
学习go语言编写Python扩展,可以参考官方文档:
- https://golang.org/cmd/cgo/
- https://docs.python.org/3/c-api/index.html
Python 为主导的项目中引入 Go 有以下几种方式:
- 将 Go 源文件编译成动态库,然后直接通过 Python 的 ctypes 模块调用
- 将 Go 源文件编译成动态库或者静态库,再结合 Cython 生成对应的 Python 扩展模块,然后直接 import 即可
- 将 Go 源文件直接编译成 Python 扩展模块,当然这要求在使用 CGO 的时候需要遵循 Python 提供的 C API
Python拓展模块是用其它语言(通常是C/C++)编写,并可以在Python中导入和使用的模块。
2. go 语言开发 Python 扩展思路
拓展模块的开发步骤是:
- 用C/C++编写模块内容,并导出Python可调用的函数。
- 编写模块初始化函数,如:
PyMODINIT_FUNC PyInit_example(void)
{// ...
}
- 使用Python C API定义模块的方法表、类型对象等。
// example.c
PyMODINIT_FUNC PyInit_example(void)
{static PyMethodDef methods[] = {{"add", add, METH_VARARGS, "Add two numbers"},{NULL, NULL, 0, NULL}};static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,"example", /* module name */ NULL, /* docstring */-1, /* size of per-interpreter state */ methods };return PyModule_Create(&moduledef);
}static PyObject* add(PyObject* self, PyObject* args)
{int a, b;if (!PyArg_ParseTuple(args, "ii", &a, &b)) {return NULL;}return PyLong_FromLong(a + b);
}
- 编译成动态库(.so文件或.pyd文件)。
- 在Python中导入和使用这个模块。
import exampleexample.add(1, 2) # Returns 3
调用add方法,它实际上会执行C代码中的add函数。
使用go原理类似:
- 编写go源代码,使用cgo编译生成共享库。cgo可以调用C语言的函数,所以go代码可以调用Python C API与Python进行交互。
- 导入C包,用于cgo调用Python C API
3. go编写python2拓展模块 示例
使用python.go和foo.go实现的简单Python扩展示例:
python.go:
package pymodule/*
#include <Python.h>
*/
import "C"
import "unsafe"//export InitModule
func InitModule() {C.Py_InitModule("pymodule", Methods)
}var Methods = []*C.PyMethodDef{{"say_hello", (C.PyCFunction)(C.PyCFunctionWithKeywords)(C.PyCFunctionCast)(unsafe.Pointer(C.SayHello)), METH_O, ""},
}
普通的 Go 只是多了一句 import “C”,除此之外没有任何和 CGO 相关的代码,也没有调用 CGO 的相关函数。但是由于这个 import,会使得 go build 命令在编译和链接阶段启动 gcc 编译器。
C 代码要通过注释的形式写在 import “C” 这行语句上方(中间不可以有空格,这是规定)。而一旦导入,就可以通过 C 这个名字空间进行调用,比如这里的 C.puts、C.CString 等等。
注意: import “C”,它不是导入一个名为 C 的包,我们可以将其理解为一个名字空间,C 语言的所有类型、函数等等都可以通过这个名字空间去调用。
编译之前,我们需要定义一些环境变量,让编译器知道在哪找Python.h
当使用 cgo 编译 go 代码生成共享库时,需要找到 Python 的头文件、库文件和 pkg-config 信息。通过设置这三个环境变量,go 的 cgo 可以找到 Python 2 的相关文件,完成 Python 扩展的编译。
如果不设置这些环境变量,cgo 在编译时很可能找不到 Python 的依赖,导致编译失败。
- PKG_CONFIG_PATH: 指定 pkg-config 工具搜索 *.pc 文件的路径。这里指定了 Python 的 pkg-config 文件路径。
- LD_LIBRARY_PATH: 指定动态链接库搜索路径。这里指定了 Python 的库文件路径。
- C_INCLUDE_PATH: 指定 C/C++ 头文件搜索路径。这里添加了 Python的 include 目录。
下面配置很重要:
export PKG_CONFIG_PATH=/home/test/env/python2/lib/pkgconfig/
export LD_LIBRARY_PATH=/home/test/env/python2/lib/export C_INCLUDE_PATH=/home/test/env/python2/include/python2.7
foo.go:
package pymodule//export SayHello
func SayHello(name *C.char) *C.char {return C.CString("Hello, " + C.GoString(name) + "!")
}
编译
go build -buildmode=c-shared -ldflags '-s -w' -o pymodule.so python.go foo.go
在Python中使用:
import pymodulepymodule.say_hello('John') # Prints "Hello, John!"
- 在python.go中定义了InitModule函数来初始化模块,并定义了方法列表Methods
- 在foo.go中定义并导出SayHello函数
- 编译两个文件生成共享库pymodule.so
- 在Python中导入pymodule模块,调用say_hello方法
- say_hello方法会调用foo.go中的SayHello函数
- SayHello函数接收来自Python的name,并返回拼接的问候语
所以,这个示例通过cgo实现了一个简单的Python扩展,导出一个say_hello方法,用于向指定的人问好。
4. go编写python3拓展模块 示例
Python 2和Python 3的C API在许多方面都发生了变化,导致同样的C代码在Python 2和3环境下编译结果不同。
Python 2和Python 3的一些主要C API变化包括:
- Py_InitModule被PyModule_Create替代。
- PyArg_ParseTuple的格式字符串发生变化。
- Py_BuildValue的格式字符串发生变化。
- 其他许多C API也发生较大变化。
package main/*
#cgo pkg-config: python3
#define Py_LIMITED_API
#include <Python.h>static PyObject *sayHello(PyObject *self, PyObject *args) {return PyUnicode_FromString("Hello from C!");
}static PyMethodDef FooMethods[] = {{"sayHello", sayHello, METH_NOARGS, "Say hello"},{NULL, NULL, 0, NULL}
};static struct PyModuleDef hellomodule = {PyModuleDef_HEAD_INIT,"hello", NULL, -1, NULL
};PyMODINIT_FUNC PyInit_hello()
{PyModuleDef_Init(&hellomodule);hellomodule.m_methods = FooMethods;PyObject *m = PyModule_Create(&hellomodule);return m;
}
*/
import "C"func main() {}
注释这部分代码实际上是C语言,只不过它调用了Go导出的sayHello函数。
编译时cgo会解析C注释,并使用其中的实现生成Python扩展。
- 定义sayHello方法,返回"Hello from C!"。
- 定义FooMethods方法数组,指定sayHello方法。
- 定义hellomodule模块。
- 实现PyInit_hello初始化函数,设置m_methods并创建模块。
这段代码使用了Python C API中的几个重要函数:
- PyUnicode_FromString:构造一个Python字符串对象。我们使用它来构造"Hello from C!"返回值。
- PyArg_ParseTuple:解析Python函数的参数。这里我们没有使用它,因为sayHello是无参数方法。
- Py_BuildValue:构造Python返回值。我们也没有直接使用它,而是使用PyUnicode_FromString构造字符串对象。
- PyMethodDef:定义方法信息,我们使用它来定义sayHello方法信息。
- PyModuleDef:定义模块信息,我们使用它来定义hello模块信息。
- PyModuleDef_Init:初始化PyModuleDef实例。
- PyModule_Create:创建一个模块,传入PyModuleDef实例。
- PyMODINIT_FUNC:定义模块初始化函数返回类型。我们使用它来定义PyInit_hello的返回类型。
在Python中:
import hellohello.sayHello() # Prints "Hello from C!"
代码优化
将C代码写在注释中有一定的局限性:
- 只能写很简短的代码,不利于开发较复杂的扩展。
- 没有自动完成功能,编写不方便。
- 调试困难,不能使用Go的调试工具。
5. python拓展模块什么时候加载
Python中,扩展模块是在导入时加载的。具体 load 的过程是:
- 当我们在 Python 中导入一个扩展模块时,Python 会搜索名为 module.so(Linux) 或 module.pyd(Windows)的文件。
- 如果找到该文件,Python 会加载它,并执行里面的初始化函数。对于我们的例子,这个函数是 PyInit_hello()。
- 这个初始化函数会返回一个模块对象。Python 使用这个对象来初始化一个模块,并将它插入 sys.modules,这样在后续的导入中可以重用该模块。
- Python 中可以使用该模块,并调用它暴露的方法。
查找拓展模块so文件位置的顺序
当在 Python 中导入一个扩展模块时,Python 会按照以下顺序搜索名为 module.so 的文件:
- 当前目录。Python 会先搜索当前路径下的 module.so 文件。
- PYTHONPATH 路径。PYTHONPATH 环境变量指定的路径下搜索 module.so 文件。
- 构建路径。如果是从源码编译的 Python,会搜索构建路径下的 module.so 文件。
- 动态链接库路径。搜索系统的动态链接库路径,如 Linux 下的 /usr/lib 等。
- 创建 module.so 软链接。如果上述路径下存在 module.so 的软链接,Python 也会进行搜索。
- 报 ImportError。如果在上述所有路径下都没有找到 module.so 文件,Python 会抛出 ImportError。
所以我们可以通过: - 将 module.so 文件置于当前路径或者 PYTHONPATH 下。
- 创建 module.so 到上述路径的软链接。
- 安装 module.so 文件到系统的动态链接库路径下。
- 在源码编译 Python 时指定 module.so 路径。
来确保 Python 可以成功导入该扩展模块。
6. go和c数据结构转化对应关系
-
基本类型:
| Go | C |
| ---------- | -------------- |
| byte | char |
| rune | wchar_t |
| int | int |
| uint | unsigned int |
| int32 | int32_t |
| uint32 | uint32_t |
| int64 | int64_t |
| uint64 | uint64_t |
| float32 | float |
| float64 | double |
| complex64 | float complex |
| complex128 | double complex | -
字符串:
Go中的字符串被转化为C中的char*。在C中操作字符串时需要注意长度和空终止等。 -
切片:
Go中的切片被转化为C中的结构体:
c
struct {
void *data;
int len;
int cap;
}
data指向切片的底层数组,len是切片的长度,cap是容量。 -
结构体:
Go中的结构体会被“扁平化”转化为C,丢失结构信息。取而代之的是对应的C类型的变量。如果要在C中重构这个结构,需要定义与之对应的C结构体。 -
接口:
Go中的接口在转化为C后会丢失,需要通过其他机制来表示,如函数指针等。 -
通道:
Go中的通道无法直接在C中表示。可以在Go中以通道来发送/接受数据,在C中以其他方式来模拟通道的效果。 -
函数:
在C/Go之间转化时,需要使用//export和cgo的调用约定来定义可以相互调用的函数。函数的参数与返回值也需要满足两种语言的类型兼容要求。
7. python C 拓展模块常用api
Python C API的官方文档地址:https://docs.python.org/3/c-api/
Python语言本身是用C语言实现的。而Python C API则是在C语言中嵌入和扩展Python的接口。
Python C API包含一组在C语言中使用的宏、函数和类型等,用于创建Python的拓展模块。借助该API,我们可以在C语言中嵌入Python的解释器,调用Python的函数与对象,实现C语言与Python之间的数据交换与操作。
Python C API用于创建拓展模块,它包含许多函数,常用的一些如下:
- Py_Initialize(): 初始化Python解释器,必须调用。
当使用cgo调用Python C API时,cgo会自动在背后调用Py_Initialize()进行初始化。所以在我们的C代码中不需要显式调用。
如果不使用cgo,而是直接使用Python C API,那么需要在代码开始处调用Py_Initialize(),例如:
int main() {Py_Initialize();// ...Py_Finalize();
}
如果不使用cgo,需要在开始和结束处分别调用Py_Initialize()和Py_Finalize()。
Py_Initialize()会初始化Python解释器,设置信号处理函数等。而Py_Finalize()会正确关闭Python解释器,释放资源。
cgo是Go语言调用C语言函数的接口。它允许我们在Go源码中直接嵌入C代码,并在两种语言之间传递数据。
cgo会在背后自动调用一些初始化函数,如Py_Initialize(),这是因为:
当我们在Go代码中调用Python C API时,Python的解释器需要被初始化,否则无法工作。所以cgo会在我们的C代码被执行前自动调用Py_Initialize()进行初始化。
cgo会自动在开始和结束调用C API时分别调用Py_Initialize()和Py_Finalize()。我们不需要在C代码中显式调用。
但是我们仍需要调用Py_Finalize()确保解释器正确退出。cgo只会在程序退出时调用Py_Finalize(),而不会在每个C代码执行结束时调用。
-
Py_Finalize(): 退出Python解释器,可选调用。
-
Py_BuildValue(): 创建Python对象,根据格式字符串决定对象类型。如"i"->int,“s”->str等。
-
PyArg_ParseTuple(): 从Python传递过来的参数中提取数据,填充C变量。如"si"可以提取int和str。
-
PyModule_Create(): 创建Python模块。 python3 使用这个!
-
PyModule_AddObject(): 将对象添加到模块中。
-
PyObject_CallObject(): 调用一个Python对象(如函数)。
-
PyCallable_Check(): 检查一个对象是否可调用。
-
PyErr_Format(): 设置一个异常。
-
Py_None: None对象,用于返回None值。
-
PyTuple_Pack(): 创建元组对象。
-
PyDict_SetItemString(): 将一个值加入字典。
-
PyString_FromString(): 创建字符串对象。
-
PyInt_FromLong(): 从long创建int对象。
-
PyModule_GetDict(): 获取模块的字典。
8. python调用so go函数原理
我们的so模块是通过cgo生成的,它依赖Go语言代码。所以Python调用so模块时,实际上会触发Go语言代码的执行。
具体过程是:
- 我们使用Go语言编写cgo代码,如sayHello函数,然后编译生成so模块。
- 在Python中导入这个so模块,并调用其函数,如sayHello。
- Python调用sayHello时,会触发so模块中cgo生成的包装函数执行。
- 这个包装函数又会调用我们编写的sayHello Go函数。所以实际执行是在Go语言层。
- 我们的sayHello函数运行在一个goroutine中,这个goroutine就是Go进程。
- Go进程会随着我们的Go程序一直运行,直到程序退出。
- 我们需要在Go程序退出前调用C.Py_Finalize()关闭解释器。
加入我们的go的main函数是空的, func main() {},理论调用sayHello整个生命周期就结束了,还需要调C.Py_Finalize()关闭解释器吗?
由于我们的main函数是空的,理论上调用sayHello函数后,相应的Go进程应该就结束了。
但是,事实并非如此。这是因为:
- 假如我们在sayHello函数中,我们启动了一个goroutine执行时间consuming的操作(如睡眠)。
- 由于sayHello函数是cgo导出的,会转换为C语言风格。在C语言中不存在goroutine的概念。
- 所以,尽管我们在sayHello中启动了goroutine,但在cgo转换为C函数后,这个语义会丢失。
- 结果是,goroutine实际上启动了一个独立的Go进程,但sayHello函数无法等待它结束就返回了。
- 这样,调用sayHello后的Python程序会结束,但之前启动的Go进程却在默默运行,导致资源泄漏。
总结:所以这种情况,还是看你代码写的有没有问题。
为什么我们即使在示例代码的main函数中调用C.Py_Finalize()也无法解决资源泄露?
在Go的main函数中调用C.Py_Finalize(),理论上它应该可以结束sayHello中启动的goroutine。
但是,当Python加载我们的so库并调用sayHello函数时,会重新触发Go代码执行,启动一个全新的Go进程。而此时Go的main函数已经退出,无法再调用C.Py_Finalize()。所以新启动的Go进程会一直运行,导致资源泄漏。
总结:main函数只在编译so库时执行一次,用于初始化我们的Go代码。
main函数只在编译so库时执行一次,用于初始化我们的Go代码。
而当Python实际调用sayHello函数时,会重新触发我们的Go代码执行,启动一个全新的Go进程。
此时,这个新进程与我们编译时执行的main函数毫无关系。main函数已经退出,无法再执行任何操作。
所以,当Python调用sayHello函数时:
- 我们的Go代码会重新执行,但与编译时执行的main函数无关。
- 这个新进程无法调用main函数中定义的任何函数或变量。main函数所在的上下文已经消失。
- 只有全局变量的状态会保留在so库中,新进程可以访问。但main函数本身不会再执行。
- 所以,在这个新进程中,无法再调用C.Py_Finalize()。它必须在Python程序的入口函数中调用,与Go代码无关。
要彻底理解这个问题,需要明确:
5. 生成so库的过程与执行go代码的关系。编译时会执行一次main函数,用于初始化。
6. Python加载so库与调用cgo函数的机制。会重新触发go代码执行,启动一个全新的进程。
7. 进程的特征与上下文的概念。每个进程都有自己的上下文,无法共享main函数的上下文。
8. Cgo函数的执行过程。真正执行cgo函数的是一个全新的Go进程,与编译时无关。
总结:要避免资源泄漏,唯一的方法是:确保sayHello函数自身不会产生资源泄漏。
- sayHello函数自身不产生资源泄漏,及时回收所有资源。
- 在Python代码中调用C.Py_Finalize(),在程序结束前关闭解释器。