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


    CGO 是什么?

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

    使用

    我们可以使用

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

    一个最简单的 CGO 使用

    package main
    
    
    //#include 
    import "C"
    
    func main(){
    	C.puts(C.CString("Hello, Cgo\n"))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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/
    
    
    • 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

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

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

    package main
    
    /*
    #include 
    
    static void SayHello(const char* s) {
        puts(s);
    }
    */
    import "C"
    
    func main(){
    	C.SayHello(C.CString("hello xiaomotong study cgo\n"))
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    xmtC.h

    void SayHi(const char * str);
    
    • 1

    xmtC.c

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

    #include 
    #include "xmtC.h"
    
    void SayHi(const char * str){
        puts(str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    main.go

    package main
    
    //void SayHi(const char * str);
    import "C"
    
    func main(){
    	C.SayHi(C.CString("hello xiaomotong study cgo\n"))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    # go build
    # ls
    cgo  main.go  xmtC.c
    # ./cgo
    hello xiaomotong study cgo
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    package main
    
    //#include 
    import "C"
    
    func main(){
    	C.SayHi(C.CString("hello xiaomotong study cgo\n"))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    合并 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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ynH1W4H-1659626343404)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f70064ec5d9d4b1b9ec0ce5579f8d35f~tplv-k3u1fbpfcp-zoom-1.image)]

    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 
    import "C"
    
    • 1
    • 2
    • 3
    • 4

    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"
    
    • 1
    • 2
    • 3

    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;
    
    • 1

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

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

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

    extern void helloString(_GoString_ p0);
    
    • 1

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

    size_t _GoStringLen(_GoString_ s);
    const char *_GoStringPtr(_GoString_ s);
    
    • 1
    • 2

    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)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    需要注意如下 2 个大点:

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

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

    struct struct_TEST {
        int type;
        float f;
    };
    
    • 1
    • 2
    • 3
    • 4

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

    若结构体是这样的呢?

    struct struct_TEST {
        int type;
        float _type;
    };
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    对于枚举 enum

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

    例如枚举类型为:

    enum TEST {
        ONE,
        TWO,
    };
    
    • 1
    • 2
    • 3
    • 4

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

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

    对于联合体 union

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

    例如

    union B1 {
        int i;
        float f;
    };
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    举个例子

    package main
    
    /*
    #include 
    
    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)))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    参考资料:

    GO 高级编程

    欢迎点赞,关注,收藏

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnf9NE3P-1659626343417)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0201d40a68894f2f825d2c66ea76ada1~tplv-k3u1fbpfcp-zoom-1.image)]

    好了,本次就到这里

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

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

  • 相关阅读:
    PL7022/PL7022B原厂双节/两节锂电池串联充电IC和保护IC
    Oracle列转行SQL语句_01
    加密算法md5、AES、RSA、Base64的区别
    Python技能树——进阶语法讲解(3)
    java 容器
    PAT 1030 Travel Plan
    verilog 实现常用加法器
    使用cpolar内网穿透远程操作Excel
    【C++】string类
    计算机毕设之基于数据可视化的智慧社区内网平台python+django+mysql(含开题+源码+部署教程)
  • 原文地址:https://blog.csdn.net/m0_37322399/article/details/126168348