golang cgo

引入C代码有一下几种方式

  1. 在go源代码中通过注释方式写入C代码,在C代码结尾紧挨着写imprt “C”,表示引入C代码。
  2. 编写独立的C代码,通过在go代码中引入C的头文件来引入C函数。
  3. 通过#cgo指令引入C库,在import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。

    #cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。

    对于在cgo环境混合使用C和C++的用户来说,可能有三种不同的编译选项:其中CFLAGS对应C语言特有的编译选项、CXXFLAGS对应是C++特有的编译选项、CPPFLAGS则对应C和C++共有的编译选项。但是在链接阶段,C和C++的链接选项是通用的,因此这个时候已经不再有C和C++语言的区别,它们的目标文件的类型是相同的。

    #cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。

    build tag 是在Go或cgo环境下的C/C++文件开头的一种特殊的注释。条件编译类似于前面通过#cgo指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。但是通过#cgo指令定义宏有个限制,它只能是基于Go语言支持的windows、darwin和linux等已经支持的操作系统。如果我们希望定义一个DEBUG标志的宏,#cgo指令就无能为力了。而Go语言提供的build tag 条件编译特性则可以简单做到。

cgo类型转换规则

char ---> C.char ---> byte
signed char ---> C.schar ---> int8
unsigned char ---> C.uchar --> uint8
short int ---> C.short ---> int16
short unsigned int ---> C.ushort ---> uint16
int ---> C.int ---> int
unsigned int ---> C.uint ---> uint32
long int ---> C.long ---> int32 or int64
long unsinged int ---> C.ulong ---> uint32 or uint64
long long int ---> C.longlong ---> int64
long long unsigned int ---> C.ulonglong -> uint64
float ---> C.float ---> float32
double ---> C.double ---> float64
long double 不支持
wchar_t ---> C.wchar_t ---> float64
void * ---> unsafe.Pointer
float_Complex ---> C.complexfloat
double_Complex ---> C.complexdouble
long double_Complex 不支持
__int128_t ---> [16]byte
__uint128_t ---> [16]byte
size_t ---> C.size_t ---> uint

转换规则详细分析

  • C/GO 数组、字符串、切片
    在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。

    在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。

    Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。

    CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 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语言的malloc函数分配,最终可以通过free函数释放。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销

    reflect包中有字符串和切片的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type StringHeader struct {
    Data uintptr
    Len int
    }

    type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
    }

    如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    package main

    /*
    #include <string.h>
    char arr[10] = {1,2,3};
    char *s = "Hello";
    */
    import "C"

    import "fmt"
    import "unsafe"
    import "reflect"

    func main() {
    // 使用reflect方式
    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])

    fmt.Println("arr0: ", arr0)
    fmt.Println("arr1: ", arr1)
    fmt.Println("s0: ", s0)
    fmt.Println("s1: ", s1)
    }

    因为Go语言的字符串是只读的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。

    在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体:

    1
    2
    typedef struct { const char *p; GoInt n; } GoString;
    typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

    在C语言中可以通过GoStringGoSlice来访问Go语言的字符串和切片。如果是Go语言中数组类型,可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理,那么在C语言中不能长时间保存Go内存对象。

  • C/GO 指针、切片
    C语言和Go语言指针的转换可以看做是两种不同类型的指针之间的转换。在Go语言中我们无法在不同类型之间做转换,因此不同类型的指针之间也存在此限制。由于任意类型的指针均可和unsafe.Pointer相互转换,所以我们可以以unsafe.Pointer作为中间桥接类型实现不同类型指针之间的转换。

    原生数值类型的指针类型可按Go语法在类型前面加上,比如var p C.int。而void*比较特殊,用Go中的unsafe.Pointer表示。

    unsafe.Pointer还可以与uintptr类型做相互转换。由于unsafe.Pointer的指针类型无法做算术操作,转为uintptr后可进行算术操作。

  • C/GO 数值
    The standard C numeric types are available under the names C.char, C.schar (signed char), C.uchar (unsigned char), C.short, C.ushort (unsigned short), C.int, C.uint (unsigned int), C.long, C.ulong (unsigned long), C.longlong (long long), C.ulonglong (unsigned long long), C.float, C.double, C.complexfloat (complex float), and C.complexdouble (complex double). The C type void* is represented by Go’s unsafe.Pointer. The C types int128_t and uint128_t are represented by [16]byte.

    CGO中,虽然C语言的int固定为4字节大小,但是Go语言自己的int和uint却在32bit和64bit系统下分别对应4bytes和8bytes。如果需要在C语言中访问Go语言的int类型,可以通过GoInt类型访问,GoInt类型在CGO工具生成的_cgo_export.h头文件中定义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main

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

    int t(int a, int b){
    return a + b;
    }
    */
    import "C"

    import "fmt"

    func Add(a int, b int) int {
    return int(C.t(C.int(a),C.int(b)))
    }

    func main() {
    fmt.Println("add result:", Add(2,3))
    }
  • C/GO 结构体、联合、枚举类型
    To access a struct, union, or enum type directly, prefix it with struct, union, or enum_, as in C.struct_stat.
    The size of any C type T is available as C.sizeof_T, as in C.sizeof_struct_stat.
    Go structs cannot embed fields with C types.

    C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。结构体的内存布局按照C语言通过对齐规则,在32bitGo语言环境C语言结构体按照32bit对齐规则,在64bitGo语言环境按照64bit对齐规则。对于指定了特殊对齐规则的结构体,无法在CGO中访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package main

    /*
    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的关键字,可以通过在成员开头添加下划线来访问,例如_type。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main

    /*
    struct A {
    int type;
    };
    */
    import "C"

    import "fmt"

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

    但是如果有2个成员,一个以Go关键字命名,另一个用下划线和Go关键字命名,那么以Go关键字命名的成员将无法访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    struct A {
    int type; // type是Go语言的关键字,将屏蔽CGO对type成员的访问
    float _type;
    };
    struct A a = {1,2};
    */
    import "C"

    import "fmt"

    func main() {
    //var a C.struct_A
    fmt.Println("type: ", C.a._type)
    }

    C语言结构体中位字段对应的成员无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    struct A {
    int size: 10; // 位字段无法访问
    float arr[]; // 零长的数组也无法访问
    };
    */
    import "C"

    import "fmt"
    func main() {
    var a C.struct_A
    fmt.Println(a.size) // 错误: 位字段无法访问
    fmt.Println(a.arr) // 错误: 零长的数组也无法访问
    }

    在C语言中,无法直接访问Go语言定义的结构体类型。
    对于联合类型,可以用C.union_xxx来访问C语言定义的union xxx类型。但Go语言中并不支持C联合类型,它们会被转为对应大小的字节数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /*
    #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包强制转型为对应类型(这是性能最好的方式)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*
    #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)))
    }

    虽然unsafe包访问最简单、性能也最好,但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    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语言中,枚举类型底层对应int类型,支持负数类型的值。我们可以通过C.ONEC.TWO等直接访问定义的枚举值。

CGO内存管理大原则

go runtime只管理go的内存,不管C。反之,C只管自己的也不管go内存。所以go的内存有GC照看,C的内存要自己C.free。

坚持原创技术分享,如果觉得文章对你有帮助,给点鼓励更好!