CGO学习一,初步认知和基本数据类型转换

1,965 阅读8分钟

这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战

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语言类型
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

需要注意 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 xxC.struct_xx
union xxC.union_xx
enum xxC.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 高级编程

欢迎点赞,关注,收藏

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

好了,本次就到这里

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

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