楔子
我们都知道python的效率很低,但是好在可以和C语言无缝结合,C语言写好动态链接库之后再让python去调用简直不要太方便。但是使用C编写动态链接库也不是件容易的事情,于是笔者想到了go,go的效率虽然不如C,但是也没有逊色太多。而且go毕竟是一门高级语言,丰富的标准库、数据类型,编写动态链接库比C方便太多了。下面我们就来看看如何使用golang编写动态链接库交给python调用。
另外python和go之间进行交互,本质上是通过C语言来作为媒介的。在go中定义函数,参数和返回值需要是C中的类型,python调用时,参数和返回值也要指定为C中的类型。而这里只会介绍golang和C中的类型如何转换,至于python的类型和C的类型如何转换我只会直接拿来用,就不写注释介绍具体的用法了。因为python和C中的类型如何转换,比如如何在python中实现C中的数组、结构体等等,我在另一篇博客中https://www.cnblogs.com/traditional/p/12243307.html
已经介绍的比较详细了,如果对python类型和C的类型之间的转化不是很清楚的小伙伴,可以先去看一看。
举个小栗子
package main
import "C"
//export age_incr_1
func age_incr_1(age int) int {
return age + 1
}
func main() {
//这个main函数我们不用,但是必须要写。
}
首先import "C"
,这一行必须要有,尽管我们目前还没有使用(其实编译的时候就使用了)
。然后定义了一个函数,接收一个整型age,然后返回age + 1。注意:我们函数上面有一个//export age_incr_1
,这一行注释必须要有,即// export 函数名
,而且和函数要挨着,不能有空行,否则这个函数是不会被导出的。最后是main函数,这个main函数也是必须要有的,尽管里面可以什么都不写,但是必须要有,否则编译不通过。
然后将这个go文件编译成dll,在linux上的话就是so,一样的。不过在Windows上编译的话需要MingW64,记得安装。
go build -buildmode=c-shared -o dll文件或者so文件 go源文件
这里我们将1.go编译成了1.dll,并没有出错,然后我们使用python调用。
import ctypes
# python调用dll文件或者so文件,直接通过ctypes.CDLL("dll或so文件路径"),得到动态链接库
lib = ctypes.CDLL(r"D:\\go\\test1\\1.dll")
# 通过反射获取对应函数
func = getattr(lib, "age_incr_1")
# 传入参数执行
print(func(17)) # 18
我们看到目前是成功了的,但是这里有一个问题,那就是不同的语言拥有不同的类型。这里go的函数接收一个整型,而我们在python中也传入了一个整型,可以看到是没有出错的,说明python和go对于整型来说是可以直接转化的。但是golang中的切片呢?map呢?我可不可以直接在python中传入list和dict呢?当然我们先不考虑这些结构,就看一下字符串吧,字符串对于任何高级语言都会有。我们知道整型没有问题,那先来看看字符串会怎么样吧?
package main
import "C"
//export say
func say() string {
return "你好啊"
}
func main() {
}
这里我们不接收参数了,直接返回一个string吧,然后编译成dll。
import ctypes
lib = ctypes.CDLL(r"D:\\go\\test1\\1.dll")
func = getattr(lib, "say")
print(func()) # -1583249216
我们看到返回了一个乱七八糟的东西,说明如果没有参数、没有返回值,或者参数、返回值仅仅是int的话,两个语言还能直接传递,但是当string的时候就不行了。那这时候怎么办呢?显然需要类型转化,golang和python在定义参数、返回参数和传入参数的时候都转换成C中的类型就可以了。
基础类型转化
我们知道python的ctypes提供了很多的C的类型,那么同理go中的C模块也提供了相应的C类型。下面我们来看看这几种类型的对应关系。
虽然类型有很多,但是很多都是重复的,比如整型我们直接使用long即可,浮点型使用double,字符串使用char *,注意这里的char *支持中文。
代码演示
关于整型,我们看到有很多,我们直接用long即可;浮点型直接使用double;另外我们在golang中定义的函数名不可以和C中的关键字冲突。
package main
import "C"
import (
"fmt"
"unsafe"
)
//export number
func number(val C.long) C.long {
//C中整型可以直接和golang的整型相加
//但前提是这是个常量,如果是变量,那么需要使用C.long转化一下
var a = 1
return val + C.long(a) //golang对类型的要求很严格,这里需要转化,但如果是val + 1是可以的,因为1是个常量
}
//export decimal
func decimal(val C.double) C.double {
return val + 2.2 //对于浮点型也是需要转化,但如果是常量,也是可以直接相加
}
//export boolean
func boolean(val C._Bool) C._Bool {
//接收一个bool类型,true返回false,false返回true
var flag = bool(val)
return C._Bool(!flag)
}
//export unicode
func unicode(val *C.char) *C.char {
//注意:char *可以直接支持中文,我们这里就不用wchar_t *了
//将C中的char *转成golang的string,可以使用C.GoString
var s1 = C.GoString(val)
s1 += "古明地觉"
//golang中string转成char *,可以使用C.CString
//除此之外可以通过将string转成byte数组、再通过unsafe.Pointer获取数组第一个元素的指针再转成*C.char的方式, (*C.char)(unsafe.Pointer(&[]byte(s1)[0]))
//但是不推荐后面这种,除了麻烦之外,还有一点就是这种返回方式python调用会报错,会报出"panic: runtime error: cgo result has Go pointer"这个错误
//因此推荐C.CString()这种方式
return C.CString(s1)
}
func main() {
var a = C.long(3)
var res_a = int(number(a))
fmt.Println(res_a) //4
var b = C.double(3.14)
var res_b = float64(decimal(b))
fmt.Println(res_b) //5.34
var c = C._Bool(true)
var res_c = bool(boolean(c))
fmt.Println(res_c) //false
var d = "我永远喜欢"
var res_d = C.GoString(unicode(C.CString(d)))
fmt.Println(res_d) //我永远喜欢古明地觉
}
我们看到golang调用是没有问题的,但是使用python调用呢?我们来试试,先把这个go文件编译成dll,linux上是so。编译方式上面写了,我这里还是编译成1.dll。
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
# 我们说在传递参数的时候可以直接传入相应的类型
# 但是在获取返回值的时候,默认返回的都是整型,如果golang中返回的不是整型,那么强转成整型肯定会出问题
# 那么我们需要在调用函数之前指定返回值的类型,我们这里调用类CDLL返回的就是动态库,假设里面有一个xxx函数,返回了一个cgo中的C.double
# 那么我们就需要在调用xxx函数之前,通过go_ext.xxx.restype = c_double提前指定返回值的类型,这样才能获取正常的结果
# 我们在go中提供了如下函数
# number:接收一个c_long,返回值加1,在这里c_int和c_long和c_longlong都是可以的,我们直接使用c_long即可
go_ext.number.restype = c_long # 默认是按照c_int来解析的,所以如果go中返回的就是C中的int,那么这行可以不用写,其实返回的C中的long也是可以不用写的,但是统一标准还是写上吧
print(go_ext.number(c_long(25))) # 26
# decimal:接收一个c_double,返回值加2.2
go_ext.decimal.restype = c_double
print(go_ext.decimal(c_double(2.5))) # 4.7
# boolean:接收一个c_bool,返回相反的布尔值
go_ext.boolean.restype = c_bool
print(go_ext.boolean(c_bool(True))) # False
print(go_ext.boolean(c_bool(False))) # True
# unicode: 接收一个c_char_p,返回一个c_char_p,注意c_char_p里面的字符串要转成字节
go_ext.unicode.restype = c_char_p
# 调用函数返回的也是一个字节,我们需要再使用utf-8转回来
print(go_ext.unicode(c_char_p("我永远喜欢".encode("utf-8"))).decode("utf-8")) # 我永远喜欢古明地觉
高级类型转化
我们说,像整型、字符串、浮点型这些数据类型基本上每个语言都会有,这些类型我们就称之为基础类型,转化起来会很方便,主要是因为这些类型C中也是存在的、并且使用起来也很简单。但是像其他类型:比如golang中的array,python若是想传递肯定是对list进行加工的,再比如golang中的结构体,python中压根就不存在一个相似的类型与之对应,当然是可以通过类来实现的。像指针、数组、切片、map、结构体,这些类型我称之为高级类型。下面会一个一个介绍,当涉及到这些类型时,python和go之间应该如何传递。
指针
一旦涉及到指针操作就小心了,因为这往往会导致内存泄露。另外,在golang中我们不能返回一个go分配的指针交给其它语言来调用,但是golang自身调用是没问题的。个人猜测,原因主要是因为golang中的指针是安全的,比如一个变量究竟该分配在堆上、还是分配在栈上,golang也会进行逃逸分析,是否返回指针便是决定一个变量究竟分配在什么地方的一个主要因素。而一旦返回指针给其他语言,那么golang就无法决定这块内存究竟何时该被回收,所以golang中不允许返回指针。而且对于python来讲,golang返回一个值还是指针,对于python而言几乎没什么区别,无非是获取的方式不一样。所以我们不会在golang中返回一个指针,但是传递一个指针是可以的
package main
import "C"
//export pointer_number
func pointer_number(val *C.long) C.long {
//将值加上100
res := C.long(int(*val) + 100)
return res
}
//export pointer_unicode
func pointer_unicode(val **C.char) *C.char {
//对于字符数组来说是个例外
res := C.CString(C.GoString(*val) + "你好啊")
return res
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
res = go_ext.pointer_number(byref(c_long(100)))
print(res) # 200
go_ext.pointer_unicode.restype = c_char_p
res = go_ext.pointer_unicode(byref(c_char_p("古明地觉".encode("utf-8"))))
print(res.decode("utf-8")) # 古明地觉你好啊
数组
下面演示如何传递一个数组,与其说是数组,倒不如说是切片。因为参数不需要指定数量,但是我们不会返回一个切片或者数组,因为那没有意义。
关于数组,我们会通过unsafe包来操作。
package main
import "C"
import (
"strings"
"unsafe"
)
//export array_number
func array_number(val []C.long, size C.long) C.long {
//传递一个数组,里面全部是C.int类型,我们把它们加在一起
//因为我们不知道有多少个, 所以还必须要指定个数,因为这是python传过来的,必须指定个数,不然golang不知道
//这里使用unsafe包,所以必须要知道里面每个元素所占的大小
item_size := unsafe.Sizeof(C.long(0))
var sum int
for i := 0; i < int(size); i++ {
//得到每个元素的指针
item_p := unsafe.Pointer(uintptr(unsafe.Pointer(&val)) + item_size * uintptr(i))
sum += int(*(*C.long)(item_p))
}
return C.long(sum)
}
//export array_unicode
func array_unicode(val []*C.char, size C.long) *C.char {
item_size := unsafe.Sizeof(C.CString(""))
res := make([]string, 0)
for i := 0; i < int(size); i++ {
item_p := unsafe.Pointer(uintptr(unsafe.Pointer(&val)) + item_size * uintptr(i))
res = append(res, C.GoString(*(**C.char)(item_p)))
}
return C.CString(strings.Join(res, "--"))
}
//export array_unicode_pointer
func array_unicode_pointer(val *[]**C.char, size C.long) *C.char {
//接收一个数组指针,数组里面存的是*C.char的指针
item_size := uintptr(8) //指针一个8字节,我这里就不测试了
res := make([]string, 0)
//获取指针指向的值
val1 := *val
for i := 0; i < int(size); i++ {
item_p := unsafe.Pointer(uintptr(unsafe.Pointer(&val1)) + item_size * uintptr(i))
//此时item_p是一个**C.char的指针,所以我们应该转成***C.char
res = append(res, C.GoString(**(***C.char)(item_p)))
}
return C.CString(strings.Join(res, "--"))
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
v1 = (c_int * 4)(13212, 211, 22, 33)
print(go_ext.array_number(v1, 4)) # 13478
print(13212 + 211 + 22 + 33) # 13478
go_ext.array_unicode.restype = c_char_p
v2 = (c_char_p * 3)(c_char_p("古明地觉".encode("utf-8")),
c_char_p("古明地恋".encode("utf-8")),
c_char_p("芙兰朵露斯卡雷特".encode("utf-8")))
print(go_ext.array_unicode(v2, 3).decode("utf-8")) # 古明地觉--古明地恋--芙兰朵露斯卡雷特
go_ext.array_unicode_pointer.restype = c_char_p
# 接收的都是指针,那么我们的类型就不再是c_char_p,而是LP_c_char_p
# 但是ctypes好像没有直接提供这个类的接口,我们需要实例化一个LP_c_char_p对象然后使用type来获取
# 并且此时不可以使用byref,而是需要使用pointer
v3 = (type(pointer(c_char_p(b""))) * 3)(pointer(c_char_p("我永远".encode("utf-8"))),
pointer(c_char_p("喜欢".encode("utf-8"))),
pointer(c_char_p("satori酱~~".encode("utf-8")))
)
# 如果传递一个指针,可以使用byref,和pointer没有区别,但是作为类型只能是pointer
# 建议以后就不使用byref了,直接使用pointer即可
# 依旧是可以打印的
print(go_ext.array_unicode_pointer(byref(v3), 3).decode("utf-8")) # 我永远--喜欢--satori酱~~
关于golang的代码,如果你用golang进行测试的话,可能会报错、也有可能是我们参数当时传错了,但是使用python调用则不会。
关于我们目前只是演示参数的传递,并没有很复杂的逻辑。但是你完全可以在golang中进行复杂的操作,比如连接hive,python连接hive需要使用一个包叫做pyhive,这个包安装起来是贼特么费劲,但是golang连接却很轻松。因此你完全可以通过golang去操作,传入一个连接的ip、密码、端口、执行的语句等等,最后golang把结果返回。因此如果你会整型、字符串的传递,对于大部分操作应该是足够的,因为逻辑都是在golang里面执行的,我们只需要传递一些关键的参数即可。
结构体
传入一个结构体
结构体算是golang中最重要的成员了吧,但是golang中的结构体如果想导入和导出,不能直接使用golang中结构体,而是需要使用C中的结构体,否则会报出Go type not supported in export: struct
package main
/*
struct Girl{
char *name;
long age;
char *gender;
};
*/
import "C"
//是的你没有看错,go中可以直接写C的代码,通过注释的形式。但是有要求,这段包含C的代码的注释必须在import "C"的上方,并且不可以有空行
//并且对于结构体来说,不要使用typedef的方式,而是直接使用struct xxx{}的方式定义,那么golang便可以通过C.struct_xxx的方式来访问这个结构体
//至于为什么要这么写,我也不知道,大概这是golang的设计原则吧,就跟函数一样,如果想导出,也要在函数上方写上//export func_name
import "fmt"
//export test_struct
func test_struct(g C.struct_Girl) *C.char {
//这里的结构体就可以通过C.struct_Girl来访问
name := C.GoString(g.name)
age := int(g.age)
gender := C.GoString(g.gender)
return C.CString(fmt.Sprintf("你的名字:%s 年龄: %d 性别: %s", name, age, gender))
}
func main() {
g := C.struct_Girl{name: C.CString("satori"), age: C.long(16), gender: C.CString("female")}
fmt.Println(C.GoString(test_struct(g))) //你的名字:satori 年龄: 16 性别: female
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_long),
("gender", c_char_p)
]
go_ext.test_struct.restype = c_char_p
g = Girl(c_char_p("古明地觉".encode("utf-8")), c_long(16), c_char_p("女".encode("utf-8")))
print(go_ext.test_struct(g).decode("utf-8")) # 你的名字:古明地觉 年龄: 16 性别: 女
所以我们需要定义一个C中的结构体,如果直接使用type Girl struct {}
的方式定义的话,这个结构体是无法导出的。那么golang中结构体难道就没有办法使用啦?答案不是的,只要golang中的结构体不作为导出函数的参数、或者返回值就行。
package main
import "C"
import "fmt"
//定义两个结构体
type Girl1 struct {
name string
age int
gender string
}
type Girl2 struct {
name *C.char
age C.long
gender *C.char
}
//export test_struct1
func test_struct1(name *C.char, age C.long, gender *C.char) *C.char {
g := Girl1{C.GoString(name), int(age), C.GoString(gender)}
return C.CString(fmt.Sprintf("你的名字: %s 你的年龄: %d 你的性别: %s", g.name, g.age, g.gender))
}
//export test_struct2
func test_struct2(name *C.char, age C.long, gender *C.char) *C.char {
g := Girl2{name, age, gender}
return C.CString(fmt.Sprintf("你的名字: %s 你的年龄: %d 你的性别: %s", C.GoString(g.name), C.long(g.age), C.GoString(g.gender)))
}
func main() {
}
此时python调用是没有问题的,两个结构体成员的类型可以是go的类型也可以是C的类型,区别就是需要类型转化的地方不同罢了。golang中的结构体,它没有作为参数也没有作为返回值,所以是可以使用的,但是如果作为参数就不行了,我们必须要使用C中的结构体。
返回一个结构体
package main
/*
struct Girl{
char *name;
long age;
char *gender;
};
*/
import "C"
//export test_struct
func test_struct(name *C.char, age C.long, gender *C.char) C.struct_Girl {
g := C.struct_Girl{name, age, gender}
return g
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_long),
("gender", c_char_p)
]
go_ext.test_struct.restype = Girl
g = go_ext.test_struct(c_char_p("古明地觉".encode("utf-8")), c_long(16), c_char_p("女".encode("utf-8")))
print(g.name.decode("utf-8")) # 古明地觉
print(g.age) # 16
print(g.gender.decode("utf-8")) # 女
传入结构体指针
结构体指针我们也是可以传递的,但是不要返回一个指针,这很危险。
package main
/*
struct Girl{
char *name;
long age;
char *gender;
};
*/
import "C"
//export test_struct
func test_struct(g *C.struct_Girl) C.struct_Girl {
g1 := C.struct_Girl{}
g1.name = C.CString(C.GoString(g.name) + "酱~~")
g1.age = C.long(g.age + 1)
g1.gender = C.CString("妹子")
return g1
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_long),
("gender", c_char_p)
]
go_ext.test_struct.restype = Girl
g = Girl(c_char_p("古明地觉".encode("utf-8")), c_long(16), c_char_p("女".encode("utf-8")))
g1 = go_ext.test_struct(pointer(g))
print(g1.name.decode("utf-8")) # 古明地觉酱~~
print(g1.age) # 17
print(g1.gender.decode("utf-8")) # 妹子
传入结构体指针数组
我们传入数组里面还可以是结构体和结构体指针,但是传入结构体的时候,不知道为啥偏移量计算的不准确,我们计算出一个指定的结构体大小之后进行偏移,却偏移不到下一个位置上。但是传入指针是可以的,我们这里只介绍指针。
package main
/*
struct Girl{
char *name;
long age;
char *gender;
};
*/
import "C"
import (
"fmt"
"strings"
"unsafe"
)
//export test_struct
func test_struct(g []*C.struct_Girl, size C.long) *C.char {
//这里是指针,大小为8。但如果是结构体的话,计算的大小则为24,因为结构体里面目前3个成员,每个成员8字节
item_size := unsafe.Sizeof(&C.struct_Girl{})
res := make([]string, 0)
for i := 0; i < int(size); i++ {
//如果不是指针,那么只能获取第一个元素的位置,当我们加上指定的大小之后,会发现无法准确偏移到下一个元素上面。
item_p := unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + item_size * uintptr(i))
item := **(**C.struct_Girl)(item_p)
word := fmt.Sprintf("name: %s age: %d gender: %s", C.GoString(item.name), C.long(item.age), C.GoString(item.gender))
res = append(res, word)
}
return C.CString(strings.Join(res, "\\n"))
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_long),
("gender", c_char_p)
]
go_ext.test_struct.restype = c_char_p
g1 = Girl(c_char_p(b"satori"), c_long(16), c_char_p(b"female"))
g2 = Girl(c_char_p(b"koishi"), c_long(15), c_char_p(b"female"))
g3 = Girl(c_char_p(b"mashiro"), c_long(17), c_char_p(b"female"))
g = [pointer(g1),
pointer(g2),
pointer(g3)
]
print(go_ext.test_struct((type(pointer(Girl())) * 3)(*g), 3).decode("utf-8"))
"""
name: satori age: 16 gender: female
name: koishi age: 15 gender: female
name: mashiro age: 17 gender: female
"""
我们传入一个结构体指针数组也是可以的,但是正如上面说的,传入结构体数组则不行,因为无法准确偏移到下一个结构上面。至于为什么会不准确,暂时不知道原因,知道的小伙伴可以分享一下。
函数
关于函数,目前没有找到一个好的方法,支持python向go中传递函数。
多返回值
我们知道golang是支持多返回值的,但是python的ctypes貌似不支持获取多返回值,因此可以传入指针进行修改,比如:
package main
import "C"
//export multi_res
func multi_res(v1 *C.int, v2 **C.char){
*v1 = 123
*v2 = C.CString("hello world")
}
func main() {
}
from ctypes import *
go_ext = CDLL(r"D:\\go\\test1\\1.dll")
a = c_int(0)
b = c_char_p(b"")
go_ext.multi_res.restype = None
go_ext.multi_res(pointer(a), pointer(b))
print(a.value) # 123
print(b.value.decode("utf-8")) # hello world