基本使用

有时候我们需要使用golang去调用一些c的类库,因为使用golang重复实现一遍比较耗时,一些成熟的功能直接调用更好。当然前提是要先安装该c库。CGO可以直接用C的代码,或者C的静态库,或者动态库,当然C++也是可以的。

golang中的CGO特性,能够创建调用C代码的Go包。

package main

import "C"

func main() {

}
CGO_ENABLED=1
> go env |grep CGO
GCCGO="gccgo"
CGO_ENABLED="1"
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"

默认的C编译器是 gcc。

一旦关闭就会影响 CGO 编译。需要特别留意,交叉编译时会默认关闭 CGO。

package main

/*

#include <stdio.h>
#include <stdlib.h>

*/
import "C"

func main() {
	s := C.CString("hello world.")
	C.puts(s)
    C.free(unsafe.Pointer(s))
}
> go run main.go
hello world.
import "C"
C.puts(s)stdio.hputs()C.CString()

cgo将当前包引用的C语言符号都放到了虚拟的C包中,同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包,但是不同的Go语言包引入的虚拟的C包之间的类型是不能通用的。这个约束对于要自己构造一些cgo辅助函数时有可能会造成一点的影响。

import "C"#cgo
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"

基本数据类型

golang 和 C 的基本数值类型转换对照表如下:

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

注意 C 中的整形比如 int 在标准中是没有定义具体字长的,但一般默认认为是 4 字节,对应 CGO 类型中 C.int 则明确定义了字长是 4 ,但 golang 中的 int 字长则是 8 ,因此对应的 golang 类型不是 int 而是 int32 。为了避免误用,C 代码最好使用 C99 标准的数值类型,对应的转换关系如下:

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

C类型是使用在C代码中的,CGO类型和GO类型都是用在GO代码中的,CGO类型是在GO代码中GO类型转换成C类型然后传递给C函数使用。然后在GO代码中C函数的返回值也是CGO类型的。

package main

/*

#include <stdint.h>
static int32_t add(int32_t a, int32_t b) {
	return a + b;
}

*/
import "C"
import "fmt"

func main() {
	var a, b int32 = 1, 2
	var c int32 = int32(C.add(C.int32_t(a), C.int32_t(b)))
	fmt.Println(c)
}

字符串

c语言没有字符串类型,使用char数组代替,于是CGO提供了用于转换 Go 和 C 类型的字符串的函数,都是通过复制数据来实现。

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
C.free(unsafe.Pointer(s))C.CString()
package main

/*

#include <stdio.h>
#include <stdlib.h>

static void SayHello(const char* s) {
	puts(s);
}
*/
import "C"
import "unsafe"

func main() {
	s := C.CString("hello world.")
	C.SayHello(s)
	C.free(unsafe.Pointer(s))
}

SayHellostaticstatichello.c
#include <stdio.h>

void SayHello(const char* s) {
	puts(s);
}
SayHello
package main

/*
#include "hello.c"
*/
import "C"

func main() {
	C.SayHello(C.CString("hello world."))
}

或者模块化

hello.h
void SayHello(const char* s);
hello.c
#include "hello.h"
#include <stdio.h>

void SayHello(const char* s) {
	puts(s);
}
hello.h
#include <iostream>

extern "C" {
	#include "hello.h"
}

void SayHello(const char* s) {
	std::cout << s;
}

或者golang实现

hello.h
void SayHello(char* s);
main.go
//export SayHello
func SayHello(s *C.char) {
	fmt.Print(C.GoString(s))
}
//export SayHelloSayHello

或者简化一下

package main

//void SayHello(char* s);
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello(C.CString("Hello, World\n"))
}

//export SayHello
func SayHello(s *C.char) {
	fmt.Print(C.GoString(s))
}
_GoString_
package main

//void SayHello(_GoString_ s);
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello("Hello, World")
}

//export SayHello
func SayHello(s string) {
	fmt.Print(s)
}

切片

golang 中切片用起来有点像 C 中的数组,但实际的内存模型还是有点区别的。C 中的数组就是一段连续的内存,数组的值实际上就是这段内存的首地址。golang 切片还包含cap和len。

由于底层内存模型的差异,不能直接将 golang 切片的指针传给 C 函数调用,而是需要将存储切片数据的内部缓冲区的首地址及切片长度取出传递,然后在C代码里面重新构建数组:

package main

/*
#include <stdint.h>

static void fill_255(char* buf, int32_t len) {
	int32_t i;
	for (i = 0; i < len; i++) {
		buf[i] = 255;
	}
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	b := make([]byte, 5)
	fmt.Println(b) // [0 0 0 0 0]
	C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))
	fmt.Println(b) // [255 255 255 255 255]
}

C函数的返回值

对于有返回值的C函数,我们可以正常获取返回值。

/*
static int div(int a, int b) {
	return a/b;
}
*/
import "C"
import "fmt"

func main() {
	v := C.div(6, 3)
	fmt.Println(v) // 2
}

上面的div函数实现了一个整数除法的运算,然后通过返回值返回除法的结果。

errnoerrno

改进后的div函数实现如下:

/*
#include <errno.h>

static int div(int a, int b) {
	if(b == 0) {
		errno = EINVAL;
		return 0;
	}
	return a/b;
}
*/
import "C"
import "fmt"

func main() {
	v0, err0 := C.div(2, 1)
	fmt.Println(v0, err0) // 2 <nil>

	v1, err1 := C.div(1, 0)
	fmt.Println(v1, err1) // 0 The device does not recognize the command.
}
errnoerrno

void函数的返回值

C语言函数还有一种没有返回值类型的函数,用void表示返回值类型。一般情况下,我们无法获取void类型函数的返回值,因为没有返回值可以获取。前面的例子中提到,cgo对errno做了特殊处理,可以通过第二个返回值来获取C语言的错误状态。对于void类型函数,这个特性依然有效。

//static void noreturn() {}
import "C"
import "fmt"

func main() {
	_, err := C.noreturn()
	fmt.Println(err)
}

我们也可以尝试获取第一个返回值,它对应的是C语言的void对应的Go语言类型:

//static void noreturn() {}
import "C"
import "fmt"

func main() {
	v, _ := C.noreturn()
	fmt.Printf("%#v", v) // main._Ctype_void{}
}

关于cgo的内部实现机制可以阅读 https://www.cntofu.com/book/73/ch2-cgo/ch2-05-internal.md

链接静态库和动态库 https://www.cntofu.com/book/73/ch2-cgo/ch2-09-static-shared-lib.md