cgo详解

cgo

cgo是go提供的一种机制,使得能够在go代码中直接调用C的库函数,大大提高了效率,此外还支持在C语言中调用GO函数。

类型转换

数值类型

在Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如C.int对应C语言的int类型。

C语言类型CGO类型Go语言类型
charC.charbyte
singed charC.scharint8
unsigned charC.ucharuint8
shortC.shortint16
unsigned shortC.ushortuint16
intC.intint32
unsigned intC.uintuint32
longC.longint32
unsigned longC.ulonguint32
long long intC.longlongint64
unsigned long long intC.ulonglonguint64
floatC.floatfloat32
doubleC.doublefloat64
size_tC.size_tuint

<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。

C语言的C99标准引入的<stdint.h>头文件,Go语言类型<stdint.h>头文件类型:

C语言类型CGO类型Go语言类型
int8_tC.int8_tint8
uint8_tC.uint8_tuint8
int16_tC.int16_tint16
uint16_tC.uint16_tuint16
int32_tC.int32_tint32
uint32_tC.uint32_tuint32
int64_tC.int64_tint64
uint64_tC.uint64_tuint64

字符串切片

在CGO生成的_cgo_export.h头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:

typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

在导出的C语言函数中我们可以直接使用Go字符串和切片。假设有以下两个导出函数:

//export helloString
func helloString(s string) {}

//export helloSlice
func helloSlice(s []byte) {}

CGO生成的_cgo_export.h头文件会包含以下的函数声明:

extern void helloString(GoString p0);
extern void helloSlice(GoSlice p0);

结构体 、联合、枚举类型

结构体

在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。

/*
struct A {
	int i;
	float f;
};
*/
import "C"
import "fmt"

func main() {
	var a C.struct_A
	fmt.Println(a.i)
	fmt.Println(a.f)
}

如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:

/*
struct A {
	int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"

func main() {
	var a C.struct_A
	fmt.Println(a._type) // _type 对应 type

联合

联合类型,我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。

/*
#include <stdint.h>

union B1 {
	int i;
	float f;
};

union B2 {
	int8_t i8;
	int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
	var b1 C.union_B1;
	fmt.Printf("%T\n", b1) // [4]uint8

	var b2 C.union_B2;
	fmt.Printf("%T\n", b2) // [8]uint8
}

如果需要操作C语言的联合类型变量,一般有三种方法:第一种是在C语言中定义辅助函数;第二种是通过Go语言的"encoding/binary"手工解码成员(需要注意大端小端问题);第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。下面展示通过unsafe包访问联合类型成员的方式:

/*
#include <stdint.h>

union B {
	int i;
	float f;
};
*/
import "C"
import "fmt"

func main() {
	var b C.union_B;
	fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
	fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

枚举

我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。

/*
enum C {
	ONE,
	TWO,
};
*/
import "C"
import "fmt"

func main() {
	var c C.enum_C = C.TWO
	fmt.Println(c)
	fmt.Println(C.ONE)
	fmt.Println(C.TWO)
}

char* , 数组

/*
#include <string.h>
char arr[10];
char *s = "Hello";
*/
import "C"
import (
	"reflect"
	"unsafe"
)
func main() {
	// 通过 reflect.SliceHeader 转换
	var arr0 []byte
	var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
	arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
	arr0Hdr.Len = 10
	arr0Hdr.Cap = 10

	// 通过切片语法转换
	arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]

	var s0 string
	var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
	s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
	s0Hdr.Len = int(C.strlen(C.s))

	sLen := int(C.strlen(C.s))
    	s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen])
}

go 调用 c++

安装mingw(即gcc)

没有安装gcc会报以下错

"gcc": executable file not found in %PATH%

go 调用c函数

package main
 
//
// 引用的C头文件需要在注释中声明,紧接着注释需要有import "C",且这一行和注释之间不能有空格
//
 
/*
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void myprint(char* s) {
	printf("%s\n", s);
}
*/
import "C"
 
 
import (
	"fmt"
	"unsafe"
)
 
func main() {
	//使用C.CString创建的字符串需要手动释放。
	cs := C.CString("Hello World\n")
	C.myprint(cs)
	C.free(unsafe.Pointer(cs))
	fmt.Println("call C.sleep for 3s")
	C.sleep(3)
	return
}

go 调用c库函数

hello.c

#include <stdio.h>
void hello()
{
    printf("hello world\n"); 
}

hello.h

#ifndef HELLO_H
#define HELLO_H
 
void hello(void);
#endif

编译

gcc -c hello.c
ar -cru libhello.a hello.o
package main
 
//使用#cgo定义库路径
 
 
/*
#cgo CFLAGS: -I .
#cgo LDFLAGS: -L . -lhello
#include "hello.h"
*/
import "C"
 
func main() {
	C.hello()
}

c调用go

go导出函数给c使用

main.go

package main // 这个文件一定要在main包下面

import "C" // 这个 import 也是必须的,有了这个才能生成 .h 文件
// 下面这一行不是注释,是导出为SO库的标准写法,注意 export前面不能有空格!!!
//export hello
func hello(value string)*C.char { // 如果函数有返回值,则要将返回值转换为C语言对应的类型
    return C.CString("hello" + value)
}
func main(){
    // 此处一定要有main函数,有main函数才能让cgo编译器去把包编译成C的库
}

注:如果go函数有多个返回值,会生成一个struct,在写c代码时要用相应的struct接收,参照生成的.h文件

生成so库

go build -buildmode=c-shared -o hello.so hello.go

#include <stdio.h>
#include <string.h>
#include "hello.h" // 此处为上一步生成的.h文件

int main(){
    char c1[] = "did";
    GoString s1 = {c1,strlen(c1)};// 构建go类型
    char *c = hello(s1);
    printf("r:%s",c);
    return 0;
}

编译C代码

gcc -o c_go test.c hello.so

常见问题

多重定义

现象:我们在cgo的注释中定义遍量活函数报多重定义的错误

原因: 变量定义或函数定义不能和export在一个源文件里面,有export的源文件中只能申明不能定义。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×