前言
由于公司的Python项目中有关于支付签名与验签的模块,是自定的一些内部逻辑,基于安全性考虑, 希望改用C/C++或者Go 来重构该部分模块,做到加解签过程透明,上层代码只需要关心结果. 由于最近开始了Golang的学习,就尝试完成这部分工作,整个过程都是边踩坑边完成,下面以样例代码来分享一下整个过程的思路.
记录
Go里面需要显示的引入C
模块, 让编译器支持生成动态链接库, 并且在代码中可以使用C语言的数据类型,这个至关重要. Calling Go code from Python code 摘取一个最简单例子
//libadd.go
package main
import "C"
//export add
func add(left, right int) int {
return left + right
}
func main() {
}
go build -buildmode=c-shared -o libadd.so libadd.go
from ctypes import cdll
lib = cdll.LoadLibrary('./libadd.so')
print("Loaded go generated SO library")
result = lib.add(2, 3)
print(result)
The cgo export command is documented in
go doc cgo
, section "C references to Go". Essentially, write//export FUNCNAME
before the function definition
有这么一段话, 需要显式注释//export add
把 add函数公开给C调用
本以为很简单的就能用, 兴致满满地把例子改一下, 改为简单的处理字符串的时候, 却发现跑不起来了.
//libadd.go
package main
import "C"
//export add
func add(left, right string) string {
return left + right
}
func main() {
}
from ctypes import CDLL
lib = CDLL('./libadd.so')
print("Loaded go generated SO library")
result = lib.add("Hello", "World")
print(result)
-
这时候运行是出错的
再次翻看资料发现这么一句话:
The python code is really short and this is only passing an integer back and forth (more complex string and struct cases are much more challenging).
这说明处理字符串的时候并不是简单改成string
类型就可以.这时候翻开了BUILDING PYTHON MODULES WITH GO 1.5 , 这时能找到的最全面的资料, 可惜里面的过程都过于复杂, 整个思路是用Go去写C code, 类似写解释器一样, 去抽象出PyObject然后按照API标准来注册、处理、返回.我仅是希望以动态链接库
的方式来能调用就可以了.
我开始思考, 为何例子中使用int
类型就可以, 我改成一个简单的接收string
返回string
却一直失败. py是利用ctypes
来跟so模块进行交互, 这里存在一个代码的翻译过程 Py -> C -> Go
, 我能想到的对于字符串数据类型的处理不一样原因引起(后面事实证明了我的猜想).那么思考一下, Py中的字符串传递到Go里面去使用什么类型来接收呢? 翻阅了大量资料, 所有答案在Python Doc 官网关于ctypes
模块中有能找到.我们来看一下这图:
这里可以很清楚的看到Python3 ctypes
中字符串 bytes
和 string
是对应的两种指针类型.同时提供了argtypes
和 restype
来显式转换动态链接库中函数的参数和返回类型.(参考StackOverFlow)
这时候按照思考的流程来修改代码
//libadd.go
package main
import "C"
//export add
func add(left, right *C.char) *C.char {
// bytes对应ctypes的c_char_p类型,翻译成C类型就是 char *指针
merge := C.GoString(left) + C.GoString(right)
return C.CString(merge)
}
func main() {}
重新编译
go build -buildmode=c-shared -o libadd.so libadd.go
Python中引用
import ctypes
add = ctypes.CDLL('./libadd.so').add
# 显式声明参数和返回的期望类型
add.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
add.restype = ctypes.c_char_p
left = b"Hello"
right = b"World"
print(add(left, right))
正确输出结果:
b"HelloWorld"
就这样, 一个基本的模块就完成, 只要关注传入参数和返回结果的数据类型处理, 我只需要丰富函数的处理逻辑,Go模块中函数内部实现对于Python是透明,只要参数正确即可.其中关于 cgo更多的信息, 大家可以自行查阅Golang.org
总结
- Python与Go之间的参数传递, 处理非INT型时需要都转为对应的C类型
- ctypes需要显式地声明DLL函数的参数和返回期望的数据类型
- 注意在Python3中字符串bytes和string的区别
- Go模块需要
//export
声明外部可调用 - Go处理C的类型是需要显式转换