简介

在 Python 的开发过程中,有时因为性能原因,或出于保护代码的需要,会使用 C、Rust、Go 这样的静态语言,编写一部分代码,并交由 Python 调用。一般这样的调用过程,都是将上述语言,编译成与 C 语言兼容的动态链接库,再交由 Python 使用。本文就将使用两个小 Demo,来看看 Python 调用 Go 的全部过程。

Demo 演示

先简述一下本文演示环境, Go: v1.10.1, Python: v3.6.5, OSX: v10.13.4 。

使用 Pyhon 调用 Go,需要 3 个步骤:

  1. 编写支持动态链接库生成的 Go 代码
  2. 使用 go build 生成 .so 文件
  3. 在 Python 里通过调用 .so 文件使用 Go 代码

本文会先演示一个简单的整数相加函数, 稍后是一个稍复杂的字符串拼接函数。为什么是这样两个函数,后文会给出原因。

整数相加 Demo

首先我们使用 Go 语言编写一个简单的整数相加 Add() 函数,并演示在 Python 中调用的全过程。

1. 编写 Go 代码

// libadd.go
package main

import "C"

//export Add
func Add(a, b int) int {
    return a + b
}

func main(){}

编写符合动态链接库规范的 Go 代码,只需要满足 3 个条件:

  1. 需要 import "C",声明使用了 cgo ,这样才能编写出 C 兼容的动态链接库
  2. 需要在 Add() 函数上加 //export Add 注释,告诉编译器,Add() 可以供其他程序调用
  3. 需要一个空的 main() 函数 func main(){}

代码非常简单,输入两个 int 参数,返回一个 int 参数即可

2. 编译成动态链接库

从 1.5+ 版本开始,Go 支持将代码编译成动态链接库,只需要一行命令即可编译成动态链接库:

go build -buildmode=c-shared -o libadd.so libadd.go

编译完成后,可以看到文件夹中多了 libadd.solibadd.h 两个文件,其中 libadd.so 就是本文要用到的动态链接库。对于 Python 来说,不需要 libadd.h 文件,就可以直接使用,但是如果使用 C 或者 C++ 调用,就需要通过 #include <libadd.h> 才能进行加载。

3. 在 Python 中调用

最后就是通过 Python 调用动态链接库了,这里的关键是使用了 cdll.LoadLibrary()

from ctypes import cdll

libadd = cdll.LoadLibrary("./libadd.so")
resp = libadd.Add(222, 444)
print(resp)

整个调用过程非常简单明了,短短 3 步,就完成了一次跨语言动态链接库调用。

字符串拼接 Demo

既然如此简单,为什么还需要一个字符串拼接的 Demo 呢?原因是 C 语言的中字符串,需要自行分配和管理内存空间,而 Go 和 Python 都是通过 GC 自动管理,所以 Go 与 Python 中的字符串并不能与 C 语言直接进行交互,需要进行显式转换。

让我们重新走一遍上述 3 个步骤:

1. 编写 Go 代码

// libconcat.go
package main

// #include <stdlib.h>
import "C"
import "unsafe"

//export Concat
func Concat(str1, str2 *C.char) *C.char {
	goStr1 := C.GoString(str1)
	goStr2 := C.GoString(str2)
	concatStr := goStr1 + goStr2

	// return cgo string
	cStr := C.CString(concatStr)
	defer C.free(unsafe.Pointer(cStr))

	return cStr
}

func main(){}

可以看出,整个 Go 代码依然满足上文中的三个条件,但是稍复杂一些。而这些复杂的工作,都是为了进行 Go 与 C 之前的字符串转。

Concat() 函数中,输入与输出参数均为 C.char,这是为了与 C 兼容。而在函数内部,则需要将字符串转换成 GoString,才能进行运算。所以整个过程就是输入 C.char,转换成 C.GoString 进行运算,在转换成 C.CString 返回。

C 字符串内存管理

上述代码中,包含了 #include <stdlib.h>defer C.free(unsafe.Pointer(cStr)) ,这是因为 C 语言代码,需要自己分配字符串内存,因此在返回时,也需要进行释放,否则会存在内存泄漏问题。

注意 : 如果使用 Linux,当前版本的 Go 与 Python,需要去掉内存释放的代码才能运行,Bug 的具体原因未知。

2. 编译成动态链接库

和之前同样的代码

go build -buildmode=c-shared -o libconcat.so libconcat.go

3. 在 Python 中调用

from ctypes import cdll, c_char_p

libconcat = cdll.LoadLibrary("./libconcat.so")
libconcat.Concat.argtypes = [c_char_p, c_char_p]
libconcat.Concat.restype = c_char_p

str1 = "Python &".encode("utf-8")
str2 = " Go".encode("utf-8")
resp = libconcat.Concat(str1, str2)
resp = resp.decode("utf-8")

print(resp)

Python 的调用,和之前也略有不同,主要是需要设置 argtypesrestype ,以便在调用 libconcat.so 时,可以进行字符串格式转换。

注意 : 在 Python 中输入和输出的字符串,均需为二进制格式。在使用前要注意进行格式转换。

总结

使用 Python 调用 Go 的整个过程,还是非常简单的。只不过在处理字符串时,两端多需要和 C 语言进行格式转换,因此稍显复杂。此外,还需注意对 C 语言的内存管理,以防止内存泄漏。

总体来说,只需要遵循 Go 编写动态链接库的规范,简单 3 步,就可以轻松实现 Python 调用 Go 程序了。

参考文献

  1. cgo 官方文档
  2. 用 Golang 为 Python 编写模块
  3. Calling Go Functions from Other Languages
  4. panic: runtime error: cgo result has Go pointer