前言

​ 由于公司的Python项目中有比较关于支付签名与验签的模块, 基于安全性考虑, 希望改用C/C++或者Go 来重构该部分模块,做到加解签过程透明,上层代码只需要关心结果. 整个过程都是边踩坑边完成,下面以简易代码来记录一下整个过程的思路.

记录

​ Go里面需要显示的引入C模块, 让编译器支持生成动态链接库, 并且在代码中可以使用C语言的数据类型,这个至关重要. Calling Go code from Python code 摘取一个最简单例子

1
2
3
4
5
6
7
8
9
10
11
12
//libadd.go
package main

import "C"

//export add
func add(left, right int) int {
return left + right
}

func main() {
}
1
go build -buildmode=c-shared -o libadd.so libadd.go
1
2
3
4
5
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调用

本以为很简单的的我, 兴致满满地把例子改一下, 改为简单的处理字符串的时候, 发现怕跑不起来了.

1
2
3
4
5
6
7
8
9
10
11
12
//libadd.go
package main

import "C"

//export add
func add(left, right string) string {
return left + right
}

func main() {
}
1
2
3
4
5
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模块中有能找到.我们来参考对应表格:

ctypes type C type Python type
c_bool _Bool bool (1)
c_char char 1-character bytes object
c_wchar wchar_t 1-character string
c_byte char int
c_ubyte unsigned char int
c_short short int
c_ushort unsigned short int
c_int int int
c_uint unsigned int int
c_long long int
c_ulong unsigned long int
c_longlong __int64 or long long int
c_ulonglong unsigned __int64 or unsigned long long int
c_size_t size_t int
c_ssize_t ssize_t or Py_ssize_t int
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL terminated) bytes object or None
c_wchar_p wchar_t * (NUL terminated) string or None
c_void_p void * int or None

这里可以很清楚的看到Python3 ctypes中字符串 bytesstring 是对应的两种指针类型.同时提供了argtypesrestype 来显式转换动态链接库中函数的参数和返回类型.(参考StackOverFlow)

这这时候再改一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//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() {}

重新编译

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

Python中引用

1
2
3
4
5
6
7
8
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))

正确输出结果:

1
b"HelloWorld"

那么这时候我就可以开始模块的编写了, 只要关注传入参数和返回结果的数据类型处理, Go模块中函数内部实现该怎么写还是怎么写.关于 cgo更多的信息, 可以查阅Golang.org

总结

  1. Python与Go之间的参数传递, 处理非INT型时需要都转为对应的C类型
  2. ctypes需要显式地声明DLL函数的参数和返回期望的数据类型
  3. 注意在Python3中字符串bytes和string的区别
  4. Go模块需要//export 声明外部可调用
  5. Go处理C的类型是需要显式转换