记一个工作中遇到的问题,由于要给C、Java、Go、Python提供客户端,客户端的核心逻辑实现语言是Go,所以Python客户端最快的实现方式是直接封装Go语言逻辑,而且因为业务需要,还要提供Python3/2两个版本,先有Python3版本,后有Python2版本,因此还需要考虑在Python2上执行Python3代码的兼容性问题。 主要的解决思路是Go语言和Python语言都对底层C语言的支持粒度较好,那就通过C语言作为
中间语言
,将Go语言和Python语言桥接在一起。
一、Go语言编译
考虑如下Go语言代码,定义了四个函数,func main()
函数是必须的,否则会提示一个错误:runtime.main_main·f: function main is undeclared in the main package
。将此文件保存至exports.go
中,下文会继续使用到。
package main
import "C"
import "fmt"
//export Hello
func Hello() {
fmt.Println("Hi!")
}
//export Say
func Say(content string) {
fmt.Println("I saied:", content)
}
//export Greet
func Greet(name string) string {
return fmt.Sprintln("Nice to meet you, ", name)
}
func main() {
Hello()
}
这里的Go代码用了2处写Go代码不太常用的点:
- 导入
C
模块:启用CGO特性,用于Go语言能调用C语言函数库; - 使用
export
注释:将相关函数导出到*.h
头文件中。
1.1 构建静态链接
执行如下go语言构建命令后,你会在你的文件目录中发现多了两个文件:exports.a
,exports.h
。这两个文件就是编译构建exports.go
文件得到的静态文件和头文件。
go build -buildmode=c-archive exports.go
在exports.h
文件中,你会发现你定义的三个函数,如下所示:
#ifdef __cplusplus
extern "C" {
#endif
extern void Hello();
extern void Say(GoString content);
extern GoString Greet(GoString name);
#ifdef __cplusplus
}
#endif
1.2 构建动态链接
执行如下命令即可以编译出静态链接库,在你的构建目录中会多出两个文件:exports.h
,exports.so
。
go build --buildmode=c-shared -o exports.so exports.go
二、Python语言调用链接库
Pythong中的ctypes
模块只提供了调用动态链接库的功能,所以想用Python加载静态链接的同学可以参考附录一进行扩展阅读。
2.1 通过ctypes调用链接库
在Python3解释器上,执行如下Python语言代码即可以判断动态链接库是否被正确加载。
import ctypes
# 加载动态链接库
exqports_lib = ctypes.cdll.LoadLibrary("./exports.so")
# 执行Go语言中定义的Hello()函数
exports_lib.Hello()
但继续执行其他函数时就会触发Core Dump
异常错误,执行Say()
函数的报错如下所示。
>>> exports_lib.Say("Hi")
runtime: out of memory: cannot allocate 140732241281024-byte block (3833856 in use)
fatal error: out of memory
goroutine 17 [running, locked to thread]:
runtime.throw({0x7f1e3b643c7b?, 0x7f1e3b91ad40?})
runtime.(*mcache).allocLarge(0x7f1e3b5d1f50?, 0x7ffec707c000, 0x1)
/usr/lib/golang/src/runtime/mcache.go:235 +0x205 fp=0xc00005eaa0 sp=0xc00005ea50 pc=0x7f1e3b5d3e45
runtime.mallocgc(0x7ffec707c000, 0x0, 0x0)
/usr/lib/golang/src/runtime/malloc.go:1029 +0x57e fp=0xc00005eb18 sp=0xc00005eaa0 pc=0x7f1e3b5cbdbe
...
[5] 369280 abort (core dumped) python3
这里报了一个goroutine
协程的错误,但实际我没有调用goroutine
协程,这里我个人盲测Go语言中的CGO
特性依赖了goroutine
模块,后续有时间在展开分析。
实际上方报错的原因在于调用Go中的Say()
函数的入参是不正确的。在exports.h
文件中你可以看见Say()
函数的入参是一个GoString
类型,而且GoString
是一个含有两个属性的结构体。但我们在Python语言中调用传入的仅是一个字符串,所以会发生错误。
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif
typedef _GoString_ GoString;
extern void Say(GoString content);
此时需要将Python代码适配修改为如下代码:
import ctypes
class GoString(ctypes.Structure):
_fields_ = [
("p", ctypes.c_char_p),
("n", ctypes.c_int64)
]
exports_lib = ctypes.cdll.LoadLibrary("./exports.so")
exports_lib.Hello()
exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents.encode(), len(contents)))
重新用Python3解释器运行会发现,代码能再一次被重新执行了,执行结果输出如下所示:
Hi!
I saied: Hi
2.2 Python2上执行Python3代码兼容问题
2.2.1 bytes类型
bytes是Python3中新的特性,在Python2中没有此类型。上文中的这个示例代码在Python2中能兼容执行。
import ctypes
class GoString(ctypes.Structure):
_fields_ = [
("p", ctypes.c_char_p),
("n", ctypes.c_int64)
]
exports_lib = ctypes.cdll.LoadLibrary("./exports.so")
exports_lib.Hello()
exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents.encode(), len(contents)))
实际在Python2中执行这个示例时不需要执行contents.encode()
,即直接执行:
exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents, len(contents)))
# exports_lib.Say(GoString(contents.encode(), len(contents)))
因为在Python2和Python3中,ctypes.c_char_p
对应的Python的兼容数据类型发生了变化。在Python3中,bytes
或者None
兼容ctypes.c_char_p
类型,而在Python2中则是str
或者None
兼容ctypes.c_char_p
类型。
2.2.2 unicode类型
Python3中字符串(str
)是一个unicode类型,而在Python2中,字符串(str
)和unicode对象是两种类型。
三、参考附录
1.在Python中调用Go
2.Go build模式之c-archive,c-shared,linkshared
3.Go CGO
4.Python Ctypes
5.Go语言之父:热爱冒险,发明过航天望远镜,想用Go语言解放程序员!
6.Python3 ctypes
7.Python2 ctypes