CGO 初步认知和基本数据类型转换

CGO 是什么?

CGO 是 GO 语言里面的一个特性,CGO 属于 GOLANG 的高级用法,主要是通过使用 GOLANG 调用 CLANG 实现的程序库

使用

我们可以使用

import "C" 来使用 CGO 这个特性

一个最简单的 CGO 使用

package main


//#include <stdio.h>
import "C"

func main(){
    C.puts(C.CString("Hello, Cgo\n"))
}

import "C"的上方可以写需要导入的库 C 库,需要注释起来,CGO 会将此处的注释内容当做 C 的代码,被称为序文(preamble)

上述代码的功能解释

使用 CGO 包的 C.CString 函数将 Go 语言字符串转为 C 语言字符串

最后调用 CGO 包的 C.puts 函数向标准输出窗口打印转换后的 C 字符串

使用go build -x main.go 编译一下

加上 -x 可以打印出编译过程中执行的指令

# go build -x main.go
WORK=/tmp/go-build594331603
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/root/.cache/go-build/fb/fbb37eeb6735cb453f6d92e2e3f46f14d9dceb5baa1cdd10aae11d1d47d60e55-d
packagefile runtime/cgo=/usr/local/go/pkg/linux_amd64/runtime/cgo.a
packagefile syscall=/usr/local/go/pkg/linux_amd64/syscall.a
packagefile runtime=/usr/local/go/pkg/linux_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/linux_amd64/errors.a
packagefile internal/bytealg=/usr/local/go/pkg/linux_amd64/internal/bytealg.a
packagefile internal/oserror=/usr/local/go/pkg/linux_amd64/internal/oserror.a
packagefile internal/race=/usr/local/go/pkg/linux_amd64/internal/race.a
packagefile internal/unsafeheader=/usr/local/go/pkg/linux_amd64/internal/unsafeheader.a
packagefile sync=/usr/local/go/pkg/linux_amd64/sync.a
packagefile internal/cpu=/usr/local/go/pkg/linux_amd64/internal/cpu.a
packagefile runtime/internal/atomic=/usr/local/go/pkg/linux_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/usr/local/go/pkg/linux_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/usr/local/go/pkg/linux_amd64/runtime/internal/sys.a
packagefile internal/reflectlite=/usr/local/go/pkg/linux_amd64/internal/reflectlite.a
packagefile sync/atomic=/usr/local/go/pkg/linux_amd64/sync/atomic.a
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=Vv0to6CWqbWf5_KTN66F/K36AEO-x4qJ_LJbz5wgG/HVbBbLSaW0sTSwlN8TzN/Vv0to6CWqbWf5_KTN66F -extld=gcc /root/.cache/go-build/fb/fbb37eeb6735cb453f6d92e2e3f46f14d9dceb5baa1cdd10aae11d1d47d60e55-d
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/

尝试自己写一个 C 函数,让 GO 来调用他

Go语言环境中调用这个 SayHello函数

package main

/*
#include <stdio.h>

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

func main(){
    C.SayHello(C.CString("hello xiaomotong study cgo\n"))
}

尝试自己写一个 C 文件,然后 GO 中进行导入和调用

xmtC.h

void SayHi(const char * str);

xmtC.c

(必须是同级目录下的 .c 文件,cgo 使用 go build 编译的时候,会默认在同级目录下找.c文件进行编译,如果咱们是需要将 C 文件做成静态库 或者 动态库的方式,那么就不要将 C 的源码文件放到同级目录下了,避免重名)

#include <stdio.h>
#include "xmtC.h"

void SayHi(const char * str){
    puts(str);
}

main.go

package main

//void SayHi(const char * str);
import "C"

func main(){
    C.SayHi(C.CString("hello xiaomotong study cgo\n"))
}

直接运行go build进行编译,运行可执行程序即可

# go build
# ls
cgo  main.go  xmtC.c
# ./cgo
hello xiaomotong study cgo

通过面向C语言接口的编程技术,我们不仅仅解放了函数的实现者,同时也简化的函数的使用者。现在我们可以将 SayHi 当作一个标准库的函数使用(和puts函数的使用方式类似)

咱们也可以在 go 文件中写成这个样子

package main

//#include <xmtC.h>
import "C"

func main(){
    C.SayHi(C.CString("hello xiaomotong study cgo\n"))
}

合并 C 和 GO 的代码

Go1.10中CGO新增加了一个_GoString_预定义的C语言类型,用来表示Go语言字符串

// +build go1.10

package main

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

import (
    "fmt"
)

func main() {
    C.SayHi("hello xiaomotong study cgo\n")
}

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

上述代码的具体执行逻辑顺序是这样的:

CGO 环境

使用 CGO 需要一定的环境环境支持

  • linux 下 需要有 gcc/g++ 的编译环境
  • windows 下需要有 MinGW 工具
  • 需要把 GO 的环境变量 CGO_ENABLED 置位 1

上述的例子中,我们有几个需要注意的点:

  • import "C" 语句不能和其他的 import 语句放在一起,需要单独一行放置

  • 上述我们在GO里面传递的值,例如 C.CString("hello xiaomotong study cgo\n") 是调用了 C 的虚拟包,将字符串转换成 C 的字符串传入进去

  • Go是强类型语言

    所以 cgo 中传递的参数类型必须与声明的类型完全一致,而且传递前必须用 ”C” 中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量

    通过虚拟的 C 包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束

#cgo 用法

我们可以使用 #cgo 语句设置编译阶段和链接阶段的相关参数

  • 编译阶段的参数

主要用于定义相关宏和指定头文件检索路径

  • 链接阶段的参数

主要是指定库文件检索路径和要链接的库文件

例如我们可以这样

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

CFLAGS

  • -DPNG_DEBUG

定义宏 PNG_DEBUG ,设置为 1

  • -I

定义头文件的检索目录是 ./include

LDFLAGS

  • -L

指定链接时库文件检索目录 ,可以通过写 ${SRCDIR}来表示当前包的绝对路径

  • -l

指定链接时需要的库,此处是 png 库

条件编译 build tag

就是在我们 go build 的时候,添加一些条件参数,当然这个条件参数在对应的文件中是需要有的,

例如上述我们使用 Go1.10的时候,就在文件中添加了 // +build go1.10

我们可以这样用:

go build -tags="debug"
go build -tags="debug test"
go build -tags="linux,386"

go build 的时候加上 -tags参数,若有多个我们可以一起写,用空格间隔,表示 ,用逗号间隔表示

GO 和 C 数据类型相互转换

cgo 官方提供了如下的数据类型转换:

C语言类型 CGO类型 Go语言类型
char C.char byte
singed 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

需要注意 3 个点:

  • CGO 中,C 语言的intlong类型都是对应4个字节的内存大小,size_t类型可以当作Go语言 uint 无符号整数类型对待

  • CGO 中,C 语言的int固定为4字节的大小 , GO 语言的 intuint 却在32位和64位系统下分别对应 4 个字节和 8 个字节大小

  • 例如数据类型中间有空格,unsigned int 不能直接通过 C.unsigned int 访问,可以使用typedef关键字提供一个规则的类型命名,这样更利于在CGO中访问

字符串和切片类型

CGO生成的 _cgo_export.h 头文件中有 GO 里面字符串,切片,通道,字典,接口等数据类型对应的表示方式,但是我们一般使用有价值的就是字符串和切片了

因为 CGO 没有提供其他数据类型的辅助函数

typedef struct { const char *p; GoInt n; } GoString;

咱们导出函数的时候可以这样写:

使用 _GoString_预定义类型,这样写可以降低在 cgo 代码中可能对 _cgo_export.h 头文件产生的循环依赖的风险

_GoString_ 是 Go1.10 针对 Go 专门加的字符

extern void helloString(_GoString_ p0);

我们可以使用官方提供的函数计算字符串的长度获取字符串的地址

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

struct ,union,enum

GO 语言中访问 C 语言的 struct ,union,enum,可以查看下表的对应关系

C语言 GO 语言
struct xx C.struct_xx
union xx C.union_xx
enum xx C.enum_xx

*对于结构体 struct *

结构体的内存布局按照 C 语言的通用对齐规则

在32位Go语言环境 C 语言结构体也按照32位对齐规则,在64位Go语言环境按照64位的对齐规则

对于指定了特殊对齐规则的结构体,无法在 CGO 中访问

GO 中可以这样访问 C 的结构体

package main

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

func main() {
    var a C.struct_TEST
    a.i = 1
    a.f = 2

    fmt.Println(a.i)
    fmt.Println(a.f)
}

需要注意如下 2 个大点:

  • 结构体成员的名字和 GO 中关键字的名字一样咋处理

例如上述结构体成员名字是这样的

struct struct_TEST {
    int type;
    float f;
};

那么我们访问 type 的时候,可以这样访问a._type即可

若结构体是这样的呢?

struct struct_TEST {
    int type;
    float _type;
};

我们访问的时候仍然是这样访问, a._type,不过实际访问到的是 float _type; ,通过 GO 就没有办法访问到 int type;

GO 中也无法访问 C 中的 零长数组 和 位字段,例如

struct struct_TEST {
    int   size: 10; // 位字段无法访问
    float arr[];    // 零长的数组无法访问
};
  • 在 C 语言中,无法直接访问 Go 语言定义的结构体类型

对于枚举 enum

枚举类型底层对应int类型,支持负数类型的值 , 我们可以直接使用 C.xx 来进行访问

例如枚举类型为:

enum TEST {
    ONE,
    TWO,
};

使用这个类型我们可以用 c C.enum_TEST

给这个变量复制的时候,我们可以这样做:c = C.ONE

对于联合体 union

Go 语言中并不支持 C 语言联合类型,它们会被转为对应大小的字节数组

例如

union B1 {
    int i;
    float f;
};

union B1 会被转换成为 4 个字节大小的 字节数组[4]uint8

GO 中操作联合体变量有 3 种方式:

  • 在C语言中定义辅助函数
  • Go语言的 encoding/binary 手工解码成员(需要注意大端小端问题)
  • 使用unsafe包强制转型为对应类型

举个例子

package main

/*
#include <stdint.h>

union TEST {
    int i;
    float f;
};
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    var b C.union_TEST

    *(*C.int)(unsafe.Pointer(&b)) = 1

    fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
    fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

我们读取和写入联合体变量的时候,使用 unsafe 包性能是最好的,通过unsafe 获取指针,然后转成对应的数据类型的指针即可

参考资料:

GO 高级编程

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本作品采用《CC 协议》,转载必须注明作者和本文链接
关注微信公众号:阿兵云原生
讨论数量: 1

老哥 你为何这么优秀呢

22小时前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!