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)
}

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

[图片上传失败...(image-cb9aae-1659626330492)]

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 高级编程

欢迎点赞,关注,收藏

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

[图片上传失败...(image-c08646-1659626330492)]

好了,本次就到这里

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容