CGO学习整理

Posted by 吴俊贤 on December 13, 2018

Go调用C

go文件编写

在Go文件中,需要有一个特殊的import "C"语句,在其前面是以/**/包含起来的注释,里面是C代码。注意不能有多的*号,否则会语法错误。

然后,在该C代码段中,就可以编写C的代码,以及调用C函数库。需要注意的是,代码段不能写C++的代码。

较为简单的示例如下

package main

/*
int add(int num1, int num2) {
	return num1 + num2;
}
 */
import "C"
import "fmt"

func main() {
	fmt.Printf("%d", C.add(1,2))
}

调用C函数,使用语法C.<函数名>即可。函数名必须出现在同一个文件的C代码段中,这样才能让go代码段找到C函数的位置。

类型转换

Go数据类型不能直接用来调用Go的函数,简单的数据类型,如int,Go会自动进行转换到对应的Go类型。但是,较为复杂的类型,如C的数组,无法直接转换。

数值类型

C中简单数据类型都可以直接使用Go的转换语法转换。如下表

Go类型 C类型
C.char char
C.schar signed char
C.uchar unsigned char
C.short short
C.ushort unsigned short
C.int int
C.uint unsigned int
C.long long
C.ulong unsigned long
C.longlong long long
C.ulonglong unsigned long long
C.float float
C.double double
C.complexfloat complex float
C.complexdouble complex double
unsafe.Pointer void*
[16]byte __int128_t
[16]byte __uint128_t

复杂类型

Go可以使用C的struct、union和enum类型。这些类型,在Go中,会有前缀struct_union_enum_

struct使用与Go的使用基本一致,如

package main

/*
struct nums {
	int num1;
	int num2;
};

int addS(struct nums num) {
	return num.num1 + num.num2;
}
 */
import "C"
import "fmt"

func main() {
	n := C.struct_nums{num1: 1, num2: 2}
	fmt.Println(C.addS(n))
}

对union类型,Go无法直接使用,在Go中将会解释为该union大小的byte数组。

字符串与byte数组

Go的字符串与byte数组都会转换为C的char数组。Go提供了这几个方法转换

// Go字符串转换为C字符串。C字符串使用malloc分配,因此需要使用C.free以避免内存泄露
func C.CString(string) *C.char

// Go byte数组转换为C的数组。使用malloc分配的空间,因此需要使用C.free避免内存泄漏
func C.CBytes([]byte) unsafe.Pointer

// C字符串转换为Go字符串
func C.GoString(*C.char) string

// C字符串转换为Go字符串,指定转换长度
func C.GoStringN(*C.char, C.int) string

// C数据转换为byte数组,指定转换的长度
func C.GoBytes(unsafe.Pointer, C.int) []byte

这里有一些特别的技巧。

直接传递Go的byte数组给C,从而让C代码直接修改byte数组

假如编写了一个C函数,将传入的char数组的内容进行修改,比如转换小写为大写这种函数。如果使用CString方法,将要进行2次转换,一次从byte转换为CString,C函数处理完后,又需要转换为Go的byte数组。

为了减少这些转换,我们可以直接将byte数组传给C代码修改,通过下面的方式获取数组指针。

cstr := unsafe.Pointer(&bytes[0])

相当于我把go的byte数组第一个位置的指针传给C代码使用,与C使用char数组类似。

但是,需要注意的是,cstr是由Go进行垃圾回收的,我们不能在C里面对其进行free操作。

将Go的byte数组直接放入C指针指向的缓存区(char数组)

这个技巧跟上一个有点关系。我们可以使用上一个的方法,拿到Go的byte数组的第一位的指针,然后,使用C的memcpy函数,就能快速把该地址的内容复制到C指针指定的缓冲区里面了。

调用C++函数库

有时候,我们需要Go调用的是C++的库,而不是C编写的。

特殊指令

C调用Go

go文件编写

如果需要C调用Go的函数某些函数,我们要在Go函数的上一行,加入一个特殊的注释//export <函数名>。这样,go构建时候,才会生成对应函数的C函数,以在C文件中调用。

注意,无论go文件中有无C代码与否,要使用cgo,必须有import "C"语句。

示例如下

test.go

package main

import "C"
import "fmt"

//export funCCall
func funCCall(name *C.char) {
	fmt.Println("Hello " + C.GoString(name))
}

由于Go的语言限制(看这里),我们不能将export的函数的定义和声明放在同一个文件,意思是,带有//export func的go文件中,不能在c语言段包含extern void func(),否则会报重复定义的错误。因为

Using //export in a file places a restriction on the preamble: since it is copied into two different C output files, it must not contain any definitions, only declarations. If a file contains both definitions and declarations, then the two output files will produce duplicate symbols and the linker will fail. To avoid this, definitions must be placed in preambles in other files, or in C source files.

大意是,cgo的函数会被复制到两个不同的c文件,两个文件就会有重复的函数符号。如果同时有声明和定义,链接器将会产生错误,因为有两个文件有这个函数。因此,在一个go文件中,不能同时有定义和声明,extern void func()就是一个声明。

因此,需要分开调用的部分,如下

test_main.go

package main

/*
extern void funCCall(char*);

void callGo() {
   funCCall("Wujunxian");
}
*/
import "C"

func main() {
	C.callGo()
}

export函数参数及返回值类型

如果希望在go函数export以后,能直接使用C的数据类型进行调用,那么,在需要export的函数,参数类型需要是C数据类型对应的Go类型,如

func foo(length C.int, string *C.char) *C.char

foo函数转换后,在c的头文件就成为了

char* foo(int length, char* string);

但是,需要注意的是,Go中的函数参数,不能有const修饰符的。因此,无法实现导出的C函数中,参数类型带有const修饰符

C中的Go数据类型

从CGo编译出来的头文件,可以看到一些Go数据类型的声明语句,整理为如下的表格。

C类型 Go类型
signed char GoInt8
unsigned char GoUint8
short GoInt16
unsigned short GoUint16
int GoInt32
unsigned int GoUint32
long long GoInt64
unsigned long long GoUint64
GoInt64 GoInt
GoUint64 GoUint
SIZE_TYPE GoUintptr
float GoFloat32
double GoFloat64
float _Complex GoComplex64
double _Complex GoComplex128
GoString GoString
void* *GoMap
void* *GoChan
struct { void *t; void *v; } GoInterface
struct { void *data; GoInt len; GoInt cap; } GoSlice

还有个特殊的typedef

typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

Go字符串函数

如果直接使用Go的字符串,传递给C使用,在C中,对应的类型为_GoString_,我们可以使用下面两个函数来获取长度和数组指针。

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

示例如下

package main

/*
extern _GoString_ getS();
// //export getS
// func getS() string {
// 	return "hello world"
// }

int getLength() {
	return (int) _GoStringLen(getS());
}
*/
import "C"
import "fmt"

func main() {
	fmt.Println(C.getLength())
}

控制台输出11,就是hello world的长度。

生成so动态库与头文件

Go可以将Go语言写的cgo文件,编译成so库,并同时生成对应的头文件。

在使用之前,需要确保cgo所在的包是main包,且有main函数,即使main函数为空。

运行下面的命令

go build -buildmode=c-shared [-o <输出文件名>] <包名或文件名>

-buildmode指定构建模式,c-shared指示go构建一个c的动态库。只有go文件中那些使用//export导出的函数,才能够被调用。同时,会在生成的地方,生成对应的头文件。除了c-shared,还有c-archive等其他模式,具体看这里

如果使用-o,我们就能指定生成的so文件的名字,同时头文件也会与so文件的名字一样。