Cgo 调用实战
Cgo 的调用主要分为两种:
- 将 C 语言代码以注释的形式放入 Go 语言文件中。
- 将 C 语言代码通过共享库的形式提供给 Go 源码。
本文章中两种方式都会提到,但是主要讲第二种方法。在讲述第二种方法时,将以 Go 语言调用 C++ 库—— Armadillo 为例。
文章将先讲述 Cgo 的原理,再讲述两种方式的调用。如果只想看示例代码的可以跳到后面。
Cgo 的使用场景
在以下的一些场景中,我们可能会无法避免地使用 Cgo。
但是我们必须明白的是,Cgo 的使用需要付出一定的成本,且其复杂性极高,难以驾驭,所以需要谨慎使用。
- 为了提升局部代码性能,使用 C 代码替换一些 Go 的代码。在性能方面,C 代码之于 Go 就好比汇编代码之于 C。
- 对 Go 内存 GC 的延迟敏感,需要自己手动进行内存管理 ( 分配和释放 ) 。
- 为一些 C 语言专有的且没有 Go 替代品的库制作 Go 绑定 ( binding ) 或包装。
- 与遗留的且很难重写或替换的 C 代码进行交互。
Cgo 的原理
我们先来看一个 Cgo 调用的例子,配合这个例子讲解 Cgo 的原理。
首先看一个 Cgo 调用的 demo 代码。
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 文件相比有几处不同:
- C 代码直接出现在 Go 文件中,但是是以注释的形式。
- 在注释后我们导入了一个 C 包
import "C"
。 main
函数中通过 C 包调用了 C 代码中定义的一个函数print
。
::: import "C"
和注释之间没有空行。只有这样编译器才能够识别。 :::
写完代码后应该对代码进行编译,而对该文件的编译与常规并无不同:
go build -x -v how_cgo_works.go
-x
-v
两个参数可以输出带有 Cgo 代码的 Go 文件编译细节。
实际编译时的主要操作:
go build
调用了一个名为 Cgo 的工具。- Cgo 会识别和读取 Go 源文件:how_cgo_works.go 中的 C 代码,并将其提取后交给外部的 C 编译器 ( 例如 gcc ) 进行编译。
- 最后与 Go 源码编译后的目标文件链接成为可执行程序。
正因为这样,Go 源文件中的 C 代码要用注释包裹并放在 C 这个伪包下面,这些特殊的语法可以被 Cgo 识别。
Cgo 代码调用
Go 文件中包含 C 源码
我们可以直接将 C++ 的语言写到 .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 语言调用。
下载 Armadillo。
下载网址: Armadillo 官网 。
推荐使用 Stable Version。
下载 Lapack 和 Blas 库。
这两个库是对矩阵运算的优化,如果想要 Armadillo 有更好的表现,推荐用户下载安装。
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 语言
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 语言
# ifdef __cplusplus
extern "C" {
# endif
void logTransform ( const double* data, double* result, int size ) ;
# ifdef __cplusplus
}
# endif
# 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 的开销
调用开销。
benchmark 测试表明:通过 Cgo 调用 C 函数付出的开销是调用 Go 函数的将近 30 倍。
增加线程数量暴涨的可能性。
Go 以轻量级 goroutine 应对高并发而闻名,Go 会优化一些原本会导致线程阻塞的系统调用。但是由于 Go 无法掌控 C 空间,所以日常开发中容易在 C 空间写出导致线程阻塞的代码,使得 Go 应用进程内线程数量暴涨的可能性增加。这与 Go 承诺的轻量级并发有所背离。
失去跨平台交叉构建能力。
其他开销。
- 内存管理。Go 空间采用垃圾回收机制,C 空间则采用手工内存管理。
- Go 所拥有的强大工具链在 C 中无法施展。比如性能剖析工具,测试覆盖率工具等。
- 调试困难。
在 Cgo 的使用中必须注意内存的管理,需要及时手动释放。
贡献者
更新日志
eb6eb
-improve(docs): use pangu formatter于e402d
-improve(docs): use chinese punctuation于25255
-fix(docs): text typo于fea7c
-improve(docs): delete extra whitespace and blank lines于c1c02
-modify(docs): remanage folders and rename files于af425
-docs: update docs于90e37
-docs: update docs于120cb
-整理tag于2d37d
-整理文章代码格式于0c060
-升级主题+整理文章格式于