基本原理
package main
//int sum(int a, int b) { return a+b; }
import "C"
func main() {
println(C.sum(1, 1))
}
生成的桩代码用于实现Go语言到C语言函数跨界调用:
main.go::C.sum
_cgo_types.go::_Cfunc_sum
runtime.cgo._cgo_runtime_cgocall(_Cfunc_sum)
main.cgo.2.c::_cgo_8ded9d269593_Cfunc_sum
main.cgo.2.c::sum()
_cgo_types.go::_Cfunc_sum
return
使用方法
假设现在有一个C++的库 libeng.so
,现在展示如何使用 cgo 调用 libeng.so
中函数:
namespace eng {
void callEngine() {}
void init(string& protofile) {}
}
-
首先我们需要构建出一个不含C++语言特性的静态库
libapi.a
-
由于 cgo 不能识别 C++ 特性,因此需要将涉及到 C++ 特性的地方用C语言转化出来。这里需要构建一个
wrapper.h 将
libapi.a
中的函数转化为不含C++特性的函数。// wrapper.h #include "eng.h" void init(char* protofile) { eng::init(string(protofile)); } void callEngine() { eng::callEngine(); }
-
然后构建
libapi.a
所需要的源文件// apic.cc #include <errno.h> // 这里的 errno 可用于生成golang中调用cgo函数,返回error extern "C" { #include "wrapper.h" } void api_init() { init(); } void api_call_engine() { callEngine(); } // apic.h void api_init(); void api_call_engine();
-
使用
gcc
编译静态库gcc -c -o api.o apic.cc -I ./ ar rcs libapi.a api.o
-
-
在 go 代码中创建一个包
engine
,对外提供 go 语言风格的函数-
一般而言会创建两个
.go
文件,一个用于 cgo 代码生成闭包函数,另外一个用于封装,提供可导出函数// engine.go package engine /* #cgo CXXFLAGS: -std=c++11 #cgo CFLAGS: -Ipath_to_apic.h #cgo LDFLAGS: -L${SRCDIR}/relpath_to_api.a/ -lapi -lstdc++ #cgo LDFLAGS: -L${SRCDIR}/relpath_to_libeng.so/ -leng #include "apic.h" #include <errno.h> */ import "C" func cgo_init() error { return C.api_init() } func cgo_call_engine(s string) error { return C.api_call_engine(C.CString(s)) } // api.go package engine import "fmt" func Init() error { return cgo_init() } func CallEngine(protofile string) error { return cgo_call_engine(protofile) }
-
性能损耗
-
C语言函数调用产生的额外开销
将C语言函数调用次数压缩,尽可能少开放接口
-
C语言函数阻塞导致该线程阻塞,Go无法取得对该线程的管理,因此这个线程上面无法开辟新的goroutine
线程数可能会暴涨
-
其他开销:C语言仍需要手动GC、Go工具无法获取C语言代码的相关堆栈信息导致调试困难
分别使用cgo和cpp调用封装好的模型调用接口的耗时情况为
>>>>> cgo
[model] warm up model ok
[model] total iter numbers: 6
[model] iter: 0, time : 5409 ms
[model] iter: 1, time : 2012 ms
[model] iter: 2, time : 1848 ms
[model] iter: 3, time : 1843 ms
[model] iter: 4, time : 1844 ms
[model] iter: 5, time : 1845 ms
write done
cgo cost 32861 ms
>>>>> cpp
[model] warm up model ok
[model] total iter numbers: 6
[model] iter: 0, time : 5370 ms
[model] iter: 1, time : 1993 ms
[model] iter: 2, time : 1824 ms
[model] iter: 3, time : 1828 ms
[model] iter: 4, time : 1828 ms
[model] iter: 5, time : 1830 ms
write done
c++ cost 32804 ms
注意这里的函数为 void foo() {...}
,调用没有涉及到Go和C变量之间的类型转换。
另外,cgo 在运行时会出现到 C++ 检测不到的一些运行时错误,例如:free(): invalid pointer
参考
-
Go语言精进之路
-
C++组件的输入输出是什么?Go 是强类型语言,所以 cgo 中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C” 中的转化函数转换成对应的 C 类型,不能直接传入 Go 中类型的变量。
package main import "C" func main() { v := 42 C.printint(C.int(v)) }
例如:指针、引用、结构体等类型之间的转化
-
在Go语言中,每个Go协程都有自己的栈空间和调用栈。当调用C语言函数时,Go语言会将当前协程的栈空间和调用栈保存起来,并创建一个新的C语言栈空间和调用栈。然后,将C语言函数的参数和返回值从Go语言栈空间中复制到C语言栈空间中,并调用C语言函数。当C语言函数返回时,将返回值从C语言栈空间中复制到Go语言栈空间中,并恢复当前协程的栈空间和调用栈。
-
package main //static const char* cs = "hello"; import "C" import "./cgo_helper" func main() { cgo_helper.PrintCString(C.cs) }
这段代码是不能正常工作的,因为当前 main 包引入的
C.cs
变量的类型是当前main
包的 cgo 构造的虚拟的 C 包下的*char
类型(具体点是*C.char
,更具体点是*main.C.char
),它和 cgo_helper 包引入的*C.char
类型(具体点是*cgo_helper.C.char
)是不同的。在 Go 语言中方法是依附于类型存在的,不同 Go 包中引入的虚拟的 C 包的类型却是不同的(main.C
不等cgo_helper.C
),这导致从它们延伸出来的 Go 类型也是不同的类型(*main.C.char
不等*cgo_helper.C.char
),这最终导致了前面代码不能正常工作。有 Go 语言使用经验的用户可能会建议参数转型后再传入。但是这个方法似乎也是不可行的,因为
cgo_helper.PrintCString
的参数是它自身包引入的*C.char
类型,在外部是无法直接获取这个类型的。换言之,一个包如果在公开的接口中直接使用了*C.char
等类似的虚拟 C 包的类型,其它的 Go 包是无法直接使用这些类型的,除非这个 Go 包同时也提供了*C.char
类型的构造函数。因为这些诸多因素,如果想在 go test 环境直接测试这些 cgo 导出的类型也会有相同的限制。对C模块的封装包,导出函数的输入输出应该都要是Go语言类型的
-
#cgo
语句主要影响 CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS 和 LDFLAGS 几个编译器环境变量。LDFLAGS 用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数 (CFLAGS 用于针对 C 语言代码设置编译参数)。 -
更严谨的做法是为 C 语言函数接口定义严格的头文件,然后基于稳定的头文件实现代码。
-
使用到的调试命令:
ldd ./libs/libapi.so # 查看链接依赖 objdump -p api.a # 查看函数签名