【GO】Golang/C++混合编程 - 基础

文章系列
【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函数,那么在最终的链接阶段将会出现符号重名的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容