Skip to content

Cgo 调用实战

2069字约7分钟

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 调用的例子, 配合这个例子讲解 Cgo 的原理.

首先看一个 Cgo 调用的 demo 代码.

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.

::: import "C" 和注释之间没有空行. 只有这样编译器才能够识别. :::

写完代码后应该对代码进行编译, 而对该文件的编译与常规并无不同:

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 源码

我们可以直接将 C++ 的语言写到 .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 build 命令编译:

go build -x -v how_cgo_works.go

在 Go 中链接外部 C 库

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

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

接下来以我使用 Cgo 调用 C++ 库-- Armadillo 为例展示整个过程. 我们主要需要准备 2 个部分:

静态文件

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

  1. 下载 Armadillo.

    下载网址: Armadillo官网.

    推荐使用 Stable Version.

  2. 下载 Lapack 和 Blas 库.

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

  3. Cmake 安装.

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

代码

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

我们这里实现一个计算 log 的函数: logTransform().

推荐使用的项目结构

  • example
    • example_test.go
    • logTransform.cpp
    • logTransform.hpp
    • main.go
  • pkg
    • include
      • armadillo_bits
      • armadillo
    • libarmadillo.dll.a
Go语言
main.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(), "")
}
C 语言
logTransform.hpp
#ifdef __cplusplus
extern "C" {
#endif
    void logTransform(const double* data, double* result, int size);
#ifdef __cplusplus
}
#endif

相关信息

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

使用 Cgo 的开销

  1. 调用开销.

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

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

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

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

  4. 其他开销.

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

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




变更历史

最后更新于: 查看全部变更历史
  • docs: update docs

    于 2024/12/2
  • docs: update docs

    于 2024/11/19
  • 整理tag

    于 2024/11/4
  • 整理文章代码格式

    于 2024/11/1
  • 升级主题+整理文章格式

    于 2024/11/1
  • 整理文章格式

    于 2024/10/29
  • update

    于 2024/10/17
  • 新增文字+CRLF全部替换为LF

    于 2024/10/17
  • 升级主题+新增文章+修改格式

    于 2024/10/14
  • 升级版本+规整文档中的格式

    于 2024/10/11
  • 给予文件夹顺序

    于 2024/9/24
  • 调整博客分类+修改about-me.md

    于 2024/9/24
  • 增加RepoCard

    于 2024/9/23
  • first commit

    于 2024/9/20

Keep It Simple