Skip to content

cgo调用实战

1821字约6分钟

Go

2023-10-30

cgo的调用主要分为两种:

  1. 将C语言代码以注释的形式放入go语言文件中
  2. 将C语言代码通过共享库的形式提供给Go源码

本文章中两种方式都会提到, 但是主要讲第二种方法. 在讲述第二种方法时, 将以go语言调用C++库--Armadillo 为例.

文章将先讲述cgo的原理, 再讲述两种方式的调用. 如果只想看示例代码的可以跳到后面.

cgo的使用场景

在以下的一些场景中, 我们可能会无法避免地使用cgo. 但是我们必须明白的是, cgo的使用需要付出一定的成本, 且其复杂性极高, 难以驾驭, 所以需要谨慎使用.

  1. 为了提升局部代码性能, 使用C代码替换一些Go的代码. 在性能方面, C代码之于Go就好比汇编代码之于C.
  2. 对Go内存GC的延迟敏感, 需要自己手动进行内存管理(分配和释放).
  3. 为一些C语言专有的且没有Go替代品的库制作Go绑定(binding)或包装.
  4. 与遗留的且很难重写或替换的C代码进行交互.

cgo的原理

当我们写好一个使用了cgo的Go语言代码如下:

// how_cgo_works.go
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// void print(char *str){
//	printf("%s\n", str)
// }
import "C"

import "unsafe"

func main(){
    s := "Hello, Cgo"
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.print(cs)  //Hello, Cgo
}

其与常规的Go文件相比有几处不同:

  1. C代码直接出现在Go文件中, 但是是以注释的形式
  2. 在注释后(中间没有空行) 我们导入了一个C包 import "C"
  3. main函数中通过C包调用了C代码中定义的一个函数 print

对该文件的编译与常规并无不同:

go build -x -v how_cgo_works.go

提示

-x -v 两个参数可以输出带有cgo代码的Go文件编译细节

实际编译时的主要操作:

  1. go build调用了一个名为cgo的工具
  2. cgo会识别和读取Go源文件(how_cgo_works.go)中的C代码, 并将其提取后交给外部的C编译器(例如gcc)进行编译
  3. 最后与Go源码编译后的目标文件链接成为可执行程序

正因为这样, Go源文件中的C代码要用注释包裹并放在C这个伪包下面, 这些特殊的语法可以被cgo识别.

cgo代码调用

Go文件中包含C源码

// how_cgo_works.go
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// void print(char *str){
//	printf("%s\n", str)
// }
import "C"

import "unsafe"

func main(){
    s := "Hello, Cgo"
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.print(cs)  //Hello, Cgo
}

直接使用正常gobuild命令编译

go build -x -v how_cgo_works.go

在Go中链接外部C库

从代码结构上来讲, 在Go源文件中大量编写C代码并不是Go推荐的惯用方法, 那么以下将展示如何将C函数和变量定义从Go源码中分离出去单独定义.

我推荐cgo的调用使用静态构建. 所谓静态构建就是指构建后的应用运行所需的所有符号、指令和数据都包含在自身的二进制文件中, 没有任何对外动态共享库的依赖. 接下来以我使用cgo调用C++库--Armadillo 为例.

1. 准备静态文件

如果想进行静态构建, 我们需要先将C++库编译成二进制文件以供Go语言调用.

1.1 下载Armadillo

下载网址: Armadillo官网

推荐使用Stable Version.

1.2 下载Lapack和Blas库

这两个库是对矩阵运算的优化, 如果想要Armadillo有更好的表现, 推荐用户下载安装.

1.3 Cmake安装

具体步骤可以参考Windows下利用CMake安装Armadillo库,包含Lapack和Blas支持库

2. 代码

我们需要撰写两个部分的代码, 一个是Go语言空间的, 一个是C语言空间的.

文件结构如下:

  • example
    • example_test.go
    • logTransform.cpp
    • logTransform.hpp
    • main.go
  • pkg
    • include
      • armadillo_bits
      • armadillo
    • libarmadillo.dll.a
2.1 Go语言
package main

// #cgo CXXFLAGS: -I../pkg/include -std=c++11
// #cgo LDFLAGS: -L../pkg/ -larmadillo
// #include "logTransform.hpp"
import "C"
import (
	"fmt"
	"time"
	"unsafe"
)

func main() {
	data := make([]float64, 0)
	for i := -10; i < 1000; i++ {
		data = append(data, float64(i))
	}
	fmt.Println(data)

	// 将 Go 切片转换为 C 数组
	dataPtr := (*C.double)(unsafe.Pointer(&data[0]))

	// 创建用于接收结果的 C 数组
	result := make([]float64, len(data))
	resultPtr := (*C.double)(unsafe.Pointer(&result[0]))
	st := time.Now()
	// 调用 C 的包装函数
	C.logTransform(dataPtr, resultPtr, C.int(len(data)))
	dur := time.Since(st)
	fmt.Println(result)
	// 打印结果
	fmt.Println("耗时: ", dur.Seconds(), "")
}
2.2 C语言
  1. logTransform.hpp

    #ifdef __cplusplus
    extern "C" {
    #endif
        void logTransform(const double* data, double* result, int size);
    #ifdef __cplusplus
    }
    #endif
  2. logTransform.cpp

    #include <armadillo>
    #include "logTransform.hpp"
    
    extern "C" void logTransform(const double* data, double* result, int size) {
         // 将 C 的数组转换为 Armadillo 的向量
         arma::vec dataVec(const_cast<double*>(data), size, false, true);
    
         // 调用你的 C++ 函数
         arma::vec resultVec = arma::log(dataVec);
    
         // 将结果复制回 C 的数组
         std::memcpy(result, resultVec.memptr(), size * sizeof(double));
    }

这里分享一个我自己封装的库, 使用Cgo实现了许多基础的计算功能. clone该项目方便查看各个文件之间的结构和函数调用的关系.

使用cgo的开销

  1. 调用开销

    benchmark测试表明: 通过cgo调用C函数付出的开销是调用Go函数的将近30倍.

  2. 增加线程数量暴涨的可能性

    Go以轻量级goroutine应对高并发而闻名, Go会优化一些原本会导致线程阻塞的系统调用. 但是由于Go无法掌控C空间, 所以日常开发中容易在C空间写出导致线程阻塞的代码, 使得Go应用进程内线程数量暴涨的可能性增加. 这与Go承诺的轻量级并发有所背离

  3. 失去跨平台交叉构建能力

  4. 其他开销

    1. 内存管理. Go空间采用垃圾回收机制, C空间则采用手工内存管理.
    2. Go所拥有的强大工具链在C中无法施展. 比如性能剖析工具, 测试覆盖率工具等.
    3. 调试困难.

警告

在cgo的使用中必须注意内存的管理, 需要及时手动释放.

本文参考资料

  1. 《Go语言精进之路---从新手到高手的编程思想、方法和技巧》
  2. CGO 从入门到放弃
  3. c++ - Cgo 找不到像 "iostream" 这样的标准库