刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

    • go 泛型
    • go条件编译
    • 分布式从ACID、CAP、BASE的理论推进
    • go链接参数 ldflags
    • TCP网络连接以及TIME_WAIT的意义
    • Go异常处理
    • Go性能调优 pprof
    • Go语言设计模式
    • Go 切片的截取
    • Go runtime详解
    • go执行外部命令
    • 标准库container三剑客:head、list、ring
    • go与http代理
    • Go内存管理
    • Go垃圾回收机制
    • Go语言中的并发编程
    • Go协程调度原理及GPM模型
    • Go中逃逸现象, 变量+堆栈
    • Go面向对象的思维理解interface
    • Go中的Defer
    • Go和Python中的深浅拷贝
    • Go语言内存对齐
    • 流和IO多路复用
    • 单点Server的N种并发模型汇总
    • 控制goroutine的数量
    • 配置管理库—Viper
    • 高性能日志库zap
    • Go中的Mutex和RWMutex.md
    • sqlx的使用
    • 分布式id 库snowflake和sonyflake
    • sync.Pool 复用对象
    • sync.Once 单例模式
    • sync.Cond 条件变量
    • unsafe.Pointer 和 uintptr
    • go 信号量
    • go语言代码优化技巧
    • go 接口型函数
    • 位运算
    • cgo教程
      • 初识 cgo
      • 在go中使用cgo
        • 1. 开启cgo
        • 2. 在go里编写c代码
      • CGO 的 N 种用法
        • 1. Go 调用自定义 C 程序
        • 2. Go 调用 C 模块
        • 3. Go 调用 C++模块
        • 4. go 调用C语言动态库
        • 5. C 调用 Go 模块
        • 5.1 go 实现c的函数
        • 5.2 原生 C 调用 Go
      • CFLAGS 与 LDFLAGS
      • CGO 与 Go类型转换
        • 1. 切片
        • 2. 字符串
        • 3. 结构体,联合,枚举
        • 3.1 结构体
        • 3.2 联合
        • 3.3 枚举
        • 4. 指针
    • go调用lib和so动态库
  • go语言实现原理

  • gin框架

  • gorm

  • go测试

  • Go语言
  • go语言进阶
bigox
2023-08-21
目录

cgo教程

官方文档: https://pkg.go.dev/cmd/cgo

参考:https://zhuanlan.zhihu.com/p/349197066、https://juejin.cn/post/7047405294107754533、https://pkg.go.dev/cmd/cgo

# 初识 cgo

  • Cgo是Go语言中的一个工具,它允许在Go代码中直接调用C语言代码,并让C语言代码调用Go代码

  • demo

    package main
    
    /*
    #include <stdio.h>
    #include <stdlib.h>
    
    void myprint(char* s) {
      printf("%s\n", s);
    }
    */
    import "C"
    import "unsafe"
    
    func main() {
    	cs := C.CString("Hello from stdio")
    	C.myprint(cs)
    	C.free(unsafe.Pointer(cs))
    }
    // 输出: Hello from stdio
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • 灰常神奇

# 在go中使用cgo

# 1. 开启cgo

  • 使用go env查看, 确保 CGO_ENABLED=1

  • 导入伪包“C”

    import "C"
    
    1

# 2. 在go里编写c代码

  • 如果导入“C”之前紧跟着注释,则在编译包的 C 部分时,该注释(称为前导码)将用作标头;

  • 如果使用 C 标头,那么 注释后面要紧跟着 import "C"

    // #include <stdio.h>
    // #include <errno.h>
    import "C"
    
    1
    2
    3
  • 可以在Go文件中通过Cgo的注释块来编写C代码。使用/* */将C代码包裹起来,将C代码直接插入Go代码中; 然后可以从 Go 代码中引用这些,就好像它们是在包“C”中定义的一样。可以使用序言中声明的所有名称,即使它们以小写字母开头; 但是前导码中的静态变量不能从 Go 代码中引用;

    package main
    
    /*
    #cgo LDFLAGS: -L/usr/local/lib
    
    #include <stdio.h>
    #include <stdlib.h>
    #define REPEAT_LIMIT 3              // CGO会保留C代码块中的宏定义
    typedef struct{                     // 自定义结构体
        int repeat_time;
        char* str;
    }blob;
    int SayHello(blob* pblob) {  // 自定义函数
        for ( ;pblob->repeat_time < REPEAT_LIMIT; pblob->repeat_time++){
            puts(pblob->str);
        }
        return 0;
    }
    */
    import "C"
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        cblob := C.blob{}                               // 在GO程序中创建的C对象,存储在Go的内存空间
        cblob.repeat_time = 0
        cblob.str = C.CString("Hello, World\n")         // C.CString 会在C的内存空间申请一个C语言字符串对象,再将Go字符串拷贝到C字符串
        ret := C.SayHello(&cblob)                       // &cblob 取C语言对象cblob的地址
        fmt.Println("ret", ret)
        fmt.Println("repeat_time", cblob.repeat_time)
        C.free(unsafe.Pointer(cblob.str))               // C.CString 申请的C空间内存不会自动释放,需要显示调用C中的free释放
    }
    
    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

# CGO 的 N 种用法

# 1. Go 调用自定义 C 程序

  • demo

    package main
    
    /*
    #include <stdio.h>
    #include <stdlib.h>
    
    void myprint(char* s) {
      printf("%s\n", s);
    }
    */
    import "C"
    import "unsafe"
    
    func main() {
    	cs := C.CString("Hello from stdio")
    	C.myprint(cs)
    	C.free(unsafe.Pointer(cs))  // 由于 C 的内存空间不受 Go 的 GC 管理,因此需要显示的调用 C 语言的 free 来进行回收
    }
    // 输出: Hello from stdio
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

# 2. Go 调用 C 模块

  • hello.c 文件

    #include <stdio.h>
    int SayHello() {
        puts("Hello World");
        return 0;
    }
    
    1
    2
    3
    4
    5
  • main.go

    main 中只对 SayHello 函数进行了声明,然后再通过链接 C 程序库的方式加载函数的实现

    package main
    
    /*
    #include "hello.c"
    int SayHello();
    */
    import "C"
    import (
    	"fmt"
    )
    
    func main() {
    	ret := C.SayHello()  // Hello World
    	fmt.Println(ret)   // 0
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 3. Go 调用 C++模块

通过链接 C++程序库的方式,来实现 Go 调用 C++程序

  • hello.h

    int SayHello();
    
    1
  • hello.cpp

    #include <iostream>
    
    extern "C" {
        #include "hello.h"
    }
    
    int SayHello() {
     std::cout<<"Hello World";
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • main.go

    CGO 提供的这种面向 C 语言接口的编程方式,使得开发者可以使用是任何编程语言来对接口进行实现,只要最终满足 C 语言接口即可。

    package main
    /*
    #include "hello.h" 
    */
    import "C"
    import (
        "fmt"
    )
    
    func main() {
        ret := C.SayHello()
        fmt.Println(ret)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# 4. go 调用C语言动态库

  • 项目目录结构如下

    ├── include
    │     └── add.c
    │     └── add.h
    ├── lib
    │     └── libadd.so
    └── main.go
    
    1
    2
    3
    4
    5
    6
  • add.h

    #ifndef __ADD_H__
    #define __ADD_H__
     
    char* Add(char* src, int n);
     
    #endif
    
    
    1
    2
    3
    4
    5
    6
    7
  • add.c

    
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
     
    char* Add(char* src, int n)
    {
        char str[20];
        sprintf(str, "%d", n);
        char *result = malloc(strlen(src)+strlen(str)+1);
        strcpy(result, src);
        strcat(result, str);
        return result;
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • linux 下编译

    会在当前目录下生成 libadd.so 文件, 在 Linux 下可用 nm -D libadd.so 查看其中的方法

    gcc -fPIC -shared -o lib/libadd.so include/add.c
    
    1
  • main.go

    package main
    
    /*
    // 头文件的位置,相对于源文件是当前目录,所以是 .,头文件在多个目录时写多个  #cgo CFLAGS: ...
    #cgo CFLAGS: -I./include
    // 从哪里加载动态库,位置与文件名,-ladd 加载 libadd.so 文件
    #cgo LDFLAGS: -L./lib -ladd -Wl,-rpath,lib
    #include "add.h"
    */
    import "C"
    import "fmt"
     
    func main() {
      val := C.Add(C.CString("go"), 2023)
      fmt.Println("run c: ", C.GoString(val))
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 注意:

    • 如果把#cgo LDFLAGS: -L./lib -ladd -Wl,-rpath,lib 改为 cgo LDFLAGS: -L./lib -ladd编译不会报错,执行时会出错

      error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
      
      1
    • 设置了环境变量 LD_LIBRARY_PATH=/home/.../lib 也能让它跑起来

      LD_LIBRARY_PATH=lib/ ./demo
      
      1

# 5. C 调用 Go 模块

  • C 调用 Go 相对于 Go 调 C 来说要复杂多,可以分为两种情况
    1. 一是原生 Go 进程调用 C,C 中再反调 Go 程序。
    2. 另一种是原生 C 进程直接调用 Go。

# 5.1 go 实现c的函数

Go 程序先调用 C 的 SayHello 接口,由于 SayHello 接口链接在 Go 的实现上,又调到 Go。

看起来调起方和实现方都是 Go,但实际执行顺序是 Go 的 main 函数,调到 CGO 生成的 C 桥接函数,最后 C 桥接函数再调到 Go 的 SayHello

  • demo/hello.h

    void SayHello(char* s);
    
    1
  • demo/hello.go

    CGO 的//export SayHello 指令将 Go 语言实现的 SayHello 函数导出为 C 语言函数。这样再 Go 中调用 C.SayHello 时,最终调用的是 hello.go 中定义的 Go 函数 SayHello

    package main
    
    // #include <hello.h>
    import "C"
    import "fmt"
    
    //export SayHello   
    func SayHello(str *C.char) {
    	fmt.Println(C.GoString(str))
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • demo/main.go

    go run ..\demo\

    package main
    
    // #include "hello.h"
    import "C"
    
    func main() {
    	C.SayHello(C.CString("Hello World"))  // Hello World
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 5.2 原生 C 调用 Go

  • hello.go

    package main
    
    import "C"
    
    //export hello
    func hello(value string)*C.char {   // 如果函数有返回值,则要将返回值转换为C语言对应的类型;如果 Go 函数有多个返回值,会生成一个 C 结构体进行返回,结构体定义参考生成的.h 文件
        return C.CString("hello" + value)
    }
    func main(){
        // 此处一定要有main函数,有main函数才能让cgo编译器去把包编译成C的库
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 生成c-shared文件 go build -buildmode=c-shared -o hello.so hello.go

  • hello.c

    #include <stdio.h>
    #include <string.h>
    #include "hello.h"                       //此处为上一步生成的.h文件
    
    int main(){
        char c1[] = "did";
        GoString s1 = {c1,strlen(c1)};       //构建Go语言的字符串类型
        char *c = hello(s1);
        printf("r:%s",c);
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 编译gcc -o c_go main.c hello.so

# CFLAGS 与 LDFLAGS

  • 参数含义

    • **CFLAGS ** : 头文件的位置,相对于源文件是当前目录;头文件在多个目录时写多个 #cgo CFLAGS: ...
    • LDFLAGS :从哪里加载动态库,位置与文件名,e.g.: -ladd 加载 libadd.so 文件
    /*
    // 头文件的位置,相对于源文件是当前目录,所以是 .,头文件在多个目录时写多个  #cgo CFLAGS: ...
    #cgo CFLAGS: -I./include
    // 从哪里加载动态库,位置与文件名,-ladd 加载 libadd.so 文件
    #cgo LDFLAGS: -L./lib -ladd -Wl,-rpath,lib
    #include "add.h"
    */
    
    1
    2
    3
    4
    5
    6
    7

    在指定目录找不到对应的文件或者库时会报错!!!

  • 软件包中的所有 cgo CPPFLAG 和 CFLAGS 指令都连接起来并用于编译该软件包中的 C 文件。包中的所有 CPPFLAGS 和 CXXFLAGS 指令都连接起来,用于编译该包中的C++文件

  • 解析 cgo 指令时,任何出现的字符串 ${SRCDIR} 都将替换为包含源文件的目录的绝对路径。这允许将预编译的静态库包含在包目录中并正确链接。例如,如果 package foo 位于目录 /go/src/foo 中:

    // #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
    将扩展到=>
    // #cgo LDFLAGS: -L/go/src/foo/libs -lfoo
    
    1
    2
    3

# CGO 与 Go类型转换

  • 标准 Cgo 类型 C.char、C.schar(有符号 char)、C.uchar (无符号字符)、C.short、C.ushort (无符号短)、C.int、C.uint(无符号整数)、C.long、C.ulong (无符号长)、C.longlong(长长)、C.ulonglong(无符号长长)、C.float、C.double、C.complexfloat(复数浮点数)和C.complexdouble(复数双精度)

  • 对照关系

    C类型 Cgo类型 go类型 字节数(byte) 数值范围
    char C.char byte 1 -128~127
    signed char C.schar int8 1 -128~127
    unsigned char C.uchar uint8 1 0~255
    short int C.short int16 2 -32768~32767
    short unsigned int C.ushort uint16 2 0~65535
    int C.int int 4 -2147483648~2147483647
    unsigned int C.uint uint32 4 0~4294967295
    long int C.long int32 or int64 4 -2147483648~2147483647
    long unsigned int C.ulong uint32 or uint64 4 0~4294967295
    long long int C.longlong int64 8 -9223372036854776001~9223372036854775999
    long long unsigned int C.ulonglong uint64 8 0~18446744073709552000
    float C.float float32 4 -3.4E-38~3.4E+38
    double C.double float64 8 1.7E-308~1.7E+308
    wchar_t C.wchar_t wchar_t 2 0~65535
    void * unsafe.Pointer
  • Go 语言的 int 和 uint 在 32 位和 64 位系统下分别是 4 个字节和 8 个字节大小。它在 C 语言中的导出类型 GoInt 和 GoUint 在不同位数系统下内存大小也不同。如下是 64 位系统中,Go 数值类型在 C 语言的导出列表

    // _cgo_export.h
    typedef signed char GoInt8;
    typedef unsigned char GoUint8;
    typedef short GoInt16;
    typedef unsigned short GoUint16;
    typedef int GoInt32;
    typedef unsigned int GoUint32;
    typedef long long GoInt64;
    typedef unsigned long long GoUint64;
    typedef GoInt64 GoInt;
    typedef GoUint64 GoUint;
    typedef __SIZE_TYPE__ GoUintptr;
    typedef float GoFloat32;
    typedef double GoFloat64;
    typedef float _Complex GoComplex64;
    typedef double _Complex GoComplex128;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • 需要注意的是在 C 语言符号名前加上 *Ctype*, 便是其在 Go 中的导出名,因此在启用 CGO 特性后,Go 语言中禁止出现以*Ctype* 开头的自定义符号名,类似的还有*Cfunc*等

# 1. 切片

  • Go 中切片的使用方法类似 C 中的数组,但是内存结构并不一样

  • C 中的数组实际上指的是一段连续的内存,而 Go 的切片在存储数据的连续内存基础上,还有一个头结构体,其内存结构如下

    img

  • 因此 Go 的切片不能直接传递给 C 使用,而是需要取切片的内部缓冲区的首地址(即首个元素的地址)来传递给 C 使用。使用这种方式把 Go 的内存空间暴露给 C 使用,可以大大减少 Go 和 C 之间参数传递时内存拷贝的消耗。

  • demo

    package main
    
    /*
    int SayHello(char* buff, int len) {
        char hello[] = "Hello Cgo!";
        int movnum = len < sizeof(hello) ? len:sizeof(hello);
        memcpy(buff, hello, movnum);                        // go字符串没有'\0',所以直接内存拷贝
        return movnum;
    }
    
    */
    import "C"
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        buff := make([]byte, 8)
        C.SayHello((*C.char)(unsafe.Pointer(&buff[0])), C.int(len(buff)))
        a := string(buff)
        fmt.Println(a)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

# 2. 字符串

  • Go 的字符串与 C 的字符串在底层的内存模型不一样:

    img

  • Go 的字符串并没有以'\0' 结尾,因此使用类似切片的方式,直接将 Go 字符串的首元素地址传递给 C 是不可行的

  • cgo 给出的解决方案是标准库函数 C.CString(),它会在 C 内存空间内申请足够的空间,并将 Go 字符串拷贝到 C 空间中。因此 C.CString 申请的内存在 C 空间中,因此需要显式的调用 C.free 来释放空间

    • C.CString()的底层实现

      func _Cfunc_CString(s string) *_Ctype_char {        // 从Go string 到 C char* 类型转换
       p := _cgo_cmalloc(uint64(len(s)+1))
       pp := (*[1<<30]byte)(p)
       copy(pp[:], s)
       pp[len(s)] = 0
       return (*_Ctype_char)(p)
      }
      
      //go:cgo_unsafe_args
      func _cgo_cmalloc(p0 uint64) (r1 unsafe.Pointer) {
       _cgo_runtime_cgocall(_cgo_bb7421b6328a_Cfunc__Cmalloc, uintptr(unsafe.Pointer(&p0)))
       if r1 == nil {
        runtime_throw("runtime: C malloc failed")
       }
       return
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
  • 更高效的字符串传递方法

    • C.CString 简单安全,但是它涉及了一次从 Go 到 C 空间的内存拷贝,对于长字符串而言这会是难以忽视的开销。

    • Go 官方文档中声称 string 类型是”不可改变的“,但是在实操中可以发现,除了常量字符串会在编译期被分配到只读段,其他的动态生成的字符串实际上都是在堆上。

    • 因此如果能够获得 string 的内存缓存区地址,那么就可以使用类似切片传递的方式将字符串指针和长度直接传递给 C 使用。

    • 查阅源码,可知 String 实际上是由缓冲区首地址 和 长度构成的。这样就可以通过一些方式拿到缓存区地址。

      type stringStruct struct {
       str unsafe.Pointer  //str首地址
       len int             //str长度
      }
      
      1
      2
      3
      4
    • test11.go 将 fmt 动态生成的 string 转为自定义类型 MyString 便可以获得缓冲区首地址,将地址传入 C 函数,这样就可以在 C 空间直接操作 Go-String 的内存空间了,这样可以免去内存拷贝的消耗。

      // test11.go
      package main
      
      /*
      #include &lt;string.h>
      int SayHello(char* buff, int len) {
          char hello[] = "Hello Cgo!";
          int movnum = len &lt; sizeof(hello) ? len:sizeof(hello);
          memcpy(buff, hello, movnum);
          return movnum;
      }
      */
      import "C"
      import (
          "fmt"
          "unsafe"
      )
      
      type MyString struct {
       Str *C.char
       Len int
      }
      func main() {
          s := fmt.Sprintf("             ")
          C.SayHello((*MyString)(unsafe.Pointer(&amp;s)).Str, C.int((*MyString)(unsafe.Pointer(&amp;s)).Len))
          fmt.Print(s)
      }
      
      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
    • 这种方法背离了 Go 语言的设计理念,如非必要,不要把这种代码带入你的工程,这里只是作为一种“黑科技”进行分享。

# 3. 结构体,联合,枚举

  • cgo 中结构体,联合,枚举的使用方式类似,可以通过 C.struct_XXX 来访问 C 语言中 struct XXX 类型。union,enum 也类似

# 3.1 结构体

  • 如果结构体的成员名字中碰巧是 Go 语言的关键字,可以通过在成员名开头添加下划线来访问

  • 如果有 2 个成员:一个是以 Go 语言关键字命名,另一个刚好是以下划线和 Go 语言关键字命名,那么以 Go 语言关键字命名的成员将无法访问(被屏蔽)

  • C 语言结构体中位字段对应的成员无法在 Go 语言中访问,如果需要操作位字段成员,需要通过在 C 语言中定义辅助函数来完成。对应零长数组的成员(C 中经典的变长数组),无法在 Go 语言中直接访问数组的元素,但同样可以通过在 C 中定义辅助函数来访问。

  • 结构体的内存布局按照 C 语言的通用对齐规则,在 32 位 Go 语言环境 C 语言结构体也按照 32 位对齐规则,在 64 位 Go 语言环境按照 64 位的对齐规则。对于指定了特殊对齐规则的结构体,无法在 CGO 中访问。

  • demo

    package main
    /*
    struct Test {
        int a;
        float b;
        double type;
        int size:10;
        int arr1[10];
        int arr2[];
    };
    int Test_arr2_helper(struct Test * tm ,int pos){
        return tm->arr2[pos];
    }
    #pragma  pack(1)
    struct Test2 {
        float a;
        char b;
        int c;
    };
    */
    import "C"
    import "fmt"
    func main() {
        test := C.struct_Test{}
        fmt.Println(test.a)
        fmt.Println(test.b)
        fmt.Println(test._type)
        //fmt.Println(test.size)        // 位数据
        fmt.Println(test.arr1[0])
        //fmt.Println(test.arr)         // 零长数组无法直接访问
        //Test_arr2_helper(&test, 1)
    
        test2 := C.struct_Test2{}
        fmt.Println(test2.c)
        //fmt.Println(test2.c)          // 由于内存对齐,该结构体部分字段Go无法访问
    }
    
    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

# 3.2 联合

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

  • 如果需要操作 C 语言的联合类型变量,一般有三种方法:第一种是在 C 语言中定义辅助函数;第二种是通过 Go 语言的"encoding/binary"手工解码成员(需要注意大端小端问题);第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。

  • demo

    package main
    /*
    #include <stdint.h>
    union SayHello {
     int Say;
     float Hello;
    };
    union SayHello init_sayhello(){
        union SayHello us;
        us.Say = 100;
        return us;
    }
    int SayHello_Say_helper(union SayHello * us){
        return us->Say;
    }
    */
    import "C"
    import (
        "fmt"
        "unsafe"
        "encoding/binary"
    )
    
    func main() {
        SayHello := C.init_sayhello()
        fmt.Println("C-helper ",C.SayHello_Say_helper(&SayHello))           // 通过C辅助函数
        buff := C.GoBytes(unsafe.Pointer(&SayHello), 4)
        Say2 := binary.LittleEndian.Uint32(buff)
        fmt.Println("binary ",Say2)                 // 从内存直接解码一个int32
        fmt.Println("unsafe modify ", *(*C.int)(unsafe.Pointer(&SayHello)))     // 强制类型转换
    }
    
    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

# 3.3 枚举

  • 对于枚举类型,可以通过C.enum_xxx来访问 C 语言中定义的enum xxx结构体类型, 使用方式和 C 相同

# 4. 指针

  • 如果一个指针类型是用 type 命令在另一个指针类型基础之上构建的,换言之两个指针底层是相同完全结构的指针,那么也可以通过直接强制转换语法进行指针间的转换。在 Go 语言中两个指针的类型完全一致则不需要转换可以直接通用。

  • 但是 C 语言中,不同类型的指针是可以显式或隐式转换。cgo 经常要面对的是 2 个完全不同类型的指针间的转换,实现这一转换的关键就是 unsafe.Pointer,类似于 C 语言中的 Void*类型指针

    img

  • 使用这种方式就可以实现不同类型间的转换,如下是从 Go - int32 到 *C.char 的转换。

#Go
上次更新: 2023/09/01, 22:31:42
位运算
go调用lib和so动态库

← 位运算 go调用lib和so动态库→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式