文章系列
【GO】Golang/C++混合编程 - SWIG
【GO】Golang/C++混合编程 - 初识
【GO】Golang/C++混合编程 - 入门
【GO】Golang/C++混合编程 - 基础
【GO】Golang/C++混合编程 - 进阶一
【GO】Golang/C++混合编程 - 进阶二
【GO】Golang/C++混合编程 - 实战
Golang/C++混合编程
类型转换
CGO 是一个联通 GO 语言和 C 语言的双向桥梁,它允许 GO 语言调用 C 语言库,反之亦然。CGO 的一个核心功能就是类型转换,它允许 GO 语言和 C 语言之间的数据交换。
在 GO 语言中访问 C 语言的符号时,一般是通过虚拟的“C”包访问,比如
C.int
对应 C 语言的 int 类型。有些 C 语言的类型是由多个关键字组成,但通过虚拟的“C”包访问 C 语言类型时名称部分不能有空格字符,比如unsigned int不能直接通过C.unsigned int
访问。因此CGO为C语言的基础数值类型都提供了相应转换规则,比如C.uint对应C语言的unsigned int。
基础类型对照表
以下为CGO类型和C语言类型的对照表:
C语言类型 | CGO类型 | Go语言类型 |
---|---|---|
char | C.char | byte |
signed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | uint16 |
int | C.int | int32 |
unsigned int | C.uint | uint32 |
long | C.long | int32 |
unsigned long | C.ulong | uint32 |
long long int | C.longlong | int64 |
unsigned long long int | C.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
int8_t | C.int8_t | int8 |
uint8_t | C.uint8_t | uint8 |
int16_t | C.int16_t | int16 |
uint16_t | C.uint16_t | uint16 |
int32_t | C.int32_t | int32 |
uint32_t | C.uint32_t | uint32 |
int64_t | C.int64_t | int64 |
uint64_t | C.uint64_t | uint64 |
需要注意的是,虽然在 C 语言中int、short等类型没有明确定义内存大小,但是在 CGO 中它们的内存大小是确定的。在 CGO 中,C 语言的 int 和 long 类型都是对应4个字节的内存大小,size_t 类型可以当作 Go 语言 uint 无符号整数类型对待。
CGO 中,虽然 C 语言的 int 固定为4字节的大小,但是 Go 语言自己的 int 和 uint 却在32位和64位系统下分别对应4个字节和8个字节大小。如果需要在 C 语言中访问 Go 语言的 int 类型,可以通过 GoInt 类型访问。
CGO 头文件 "_cgo_export.h"
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;
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;
不过需要注意的是,其中只有字符串和切片在 CGO 中有一定的使用价值,因为 CGO 为他们的某些 GO 语言版本的操作函数生成了 C 语言版本,因此二者可以在 Go 调用 C 语言函数时马上使用;而 CGO 并未针对其他的类型提供相关的辅助函数,且 Go 语言特有的内存模型导致我们无法保持这些由 Go 语言管理的内存指针,所以它们 C 语言环境并无使用的价值。
结构体、联合、枚举类型
结构体
C 语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到 Go 语言的结构体中。在 Go 语言中,我们可以通过
C.struct_xxx
来访问 C 语言中定义的struct xxx
结构体类型。结构体的内存布局按照 C 语言的通用对齐规则,在32位 Go 语言环境 C 语言结构体也按照32位对齐规则,在64位 Go 语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体,无法在 CGO 中访问。
// 示例:
package main
/*
struct A {
int i;
float f;
int type; // type 是 Go 语言的关键字
float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
fmt.Println(a._type)
// 未声明 float _type 时, _type 对应 int type
// 声明 float _type 时, _type 对应 float _type
}
注:C 语言结构体中位字段对应的成员无法在 Go 语言中访问,如果需要操作位字段成员,需要通过在 C 语言中定义辅助函数来完成。对应零长数组的成员,无法在 Go 语言中直接访问数组的元素。在C语言中,我们无法直接访问Go语言定义的结构体类型。
联合
对于联合类型,我们可以通过
C.union_xxx
来访问 C 语言中定义的union xxx
类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。
// 示例:
package main
/*
#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包访问联合类型成员的方式:
fmt.Println("b1.i:", *(*C.int)(unsafe.Pointer(&b1)))
fmt.Println("b1.f:", *(*C.float)(unsafe.Pointer(&b1)))
}
枚举
对于枚举类型,我们可以通过
C.enum_xxx
来访问 C 语言中定义的enum xxx
结构体类型。
// 示例:
package main
/*
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)
}
数组、字符串和切片
C/GO 数组字符串定义
在 C 语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C 语言的字符串是一个 char 类型的数组,字符串的长度需要根据表示结尾的 NULL 字符的位置确定。C 语言中没有切片类型。
在 GO 语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。GO 语言字符串对应一段长度确定的只读 byte 类型的内存。GO 语言的切片则是一个简化版的动态数组。
相互转换
CGO 的C 虚拟包提供了以下一组函数,用于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.CString
针对输入的 GO 字符串,克隆一个 C 语言格式的字符串;返回的字符串由 C 语言的 malloc 函数分配,不使用时需要通过 C 语言的 free 函数释放。C.CBytes
函数的功能和C.CString
类似,用于从输入的 GO 语言字节切片克隆一个 C 语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString
用于将从 NULL 结尾的 C 语言字符串克隆一个 GO 语言字符串。C.GoStringN
是另一个字符数组克隆函数。C.GoBytes
用于从 C 语言数组,克隆一个 GO 语言字节切片。该组辅助函数都是以克隆的方式运行,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。
指针
在 C 语言中,不同类型的指针是可以显式或隐式转换的,如果是隐式只是会在编译时给出一些警告信息。但是 GO 语言对于不同类型的转换非常严格,任何 C 语言中可能出现的警告信息在 GO 语言中都可能是错误!指针是 C 语言的灵魂,指针间的自由转换也是 CGO 代码中经常要解决的第一个重要的问题。
CGO 存在的一个目的就是打破 GO 语言的禁止,恢复 C 语言应有的指针的自由转换和指针运算。以下代码演示了如何将X类型的指针转化为Y类型的指针:
var p *X
var q *Y
q = (*Y)(unsafe.Pointer(p)) // *X => *Y
p = (*X)(unsafe.Pointer(q)) // *Y => *X
任何类型的指针都可以通过强制转换为
unsafe.Pointer
指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。类似 C 语言的 void* 指针。
数值与指针
在 C 语言中,数值和指针之间可以相互转换,但是转换的结果可能出乎意料。在 CGO 中,数值和指针之间的转换也是被禁止的,但是通过
unsafe
包,可以绕过这个限制。GO 语言针对unsafe.Pointr
指针类型特别定义了一个uintptr
类型。我们可以uintptr
为中介,实现数值类型到unsafe.Pointr
指针类型到转换。以下代码演示了如何将数值转换为指针:
var p *X
var i uintptr
i = uintptr(unsafe.Pointer(p)) // *X => uintptr
p = (*X)(unsafe.Pointer(i)) // uintptr => *X
任何数值类型都可以通过
uintptr
类型转换为指针类型,但是转换的结果可能并不是我们期望的。数值到指针的转换,实际上是将数值解释为内存地址,然后通过这个地址去访问内存。如果数值对应的内存地址是非法的,那么转换后的指针将无法正常工作。因此,数值到指针的转换需要格外小心,必须保证数值对应的内存地址是合法的。
函数
函数是 C 语言编程的核心,通过 CGO 技术我们不仅仅可以在 GO 语言中调用 C 语言函数,也可以将Go语言函数导出为C语言函数。
GO调用C函数
package main
/*
static int div(int a, int b) {
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v := C.div(6, 3)
fmt.Println(v)
}
在 C 语言中,函数并不支持返回多个返回值。若我们期望 C 语言函数像 GO 语言函数一样同时返回结果和错误信息,则可以借助
errno.h
标准库所提供的errno
宏来实现。errno
变量是一个全局变量,当 C 语言函数执行失败时,会设置errno
变量的值为一个错误码,然后返回一个错误结果。GO 语言函数可以通过检查errno
变量的值来判断 C 语言函数是否执行成功。
package main
/*
#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)
v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}
C 语言 void 函数
C 语言函数还有一种没有返回值类型的函数,用
void
表示返回值类型。一般情况下,我们无法获取void
类型函数的返回值,因为没有返回值可以获取。前面的例子中提到,CGO 对errno
做了特殊处理,可以通过第二个返回值来获取 C 语言的错误状态。对于void
类型函数,这个特性依然有效。
C调用GO函数
CGO 还有一个强大的特性:将 GO 函数导出为 C 语言函数。
package main
import "C"
//export add
func add(a, b C.int) C.int {
return a+b
}
注:当导出 C 语言接口时,需要保证函数的参数和返回值类型都是 C 语言友好的类型,同时返回值不得直接或间接包含Go语言内存空间的指针。如果在两个不同的 GO 语言包内,都存在一个同名的要导出为 C 语言函数的
add
函数,那么在最终的链接阶段将会出现符号重名的问题。