各种原因需要与c或者c++打交道,之前对cgo有一点的了解,曾经了在了解的过程中记录了学习的过程。仅在使用的角度上讲,但是好多东西确实是模棱两可。一个契机,需要在go的框架下用到c++语言的sdk,顺便就记录一下cgo的学习过程,然后再给自己挖个坑,再深入了解一下cgo的机理和更加广泛的使用。
本篇文章主要从主调的角度入手,介绍如何在go中使用c的代码,面对工程级的如何模块化,对于小的c代码如何在一个文件中实现;介绍如何在c中使用go的导出函数,作为c函数的回调函数使用。
构造一个超级简单的cgo程序,在这个程序中我们只引入cgo的包。虽然在代码中没有用到cgo相关的代码,但是在编译和链接的阶段启动了gcc编译器。算是一个完整的cgo程序。
package main
import "C"
import "fmt"
func main() {
fmt.Println("hello world")
}
在go程序中引用c的标准库函数,打印字符串。在下面代码中C.CString("hello world")
申请了c的内存,需要手动释放掉,不释放的话会导致内存泄露。但是这个示例不影响,程序退出后系统会自动回收资源。
package main
//#include
import "C"
func main() {
C.puts(C.CString("hello world"))
}
在1.1.2中我们使用了c的标准库函数puts输出了一个字符串,在这定义一个函数,打印go输入的字符串到终端上。这个例子调用c的free函数,释放了内存,就不会存在内存泄露的问题(需要引入stdlib.h标准库)。
package main
/*
#include
#include
static void sayHello(const char* s)
{
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.sayHello(cs)
}
在1.1.3示例中定义了一个sayHello的函数,实现了打印字符串的功能,但是看起来很乱,没有模块化。我们将c函数剥离为一个.c的函数,放在与main.go同级的目录下,相应的修改main.go函数的部分代码。
//sayHello.c
#ifndef _HELLO_H
#define _HELLO_H
#include
void sayHello(const char *s)
{
puts(s);
}
#endif
package main
/*
#include
#include "sayHello.c"
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.sayHello(cs)
}
将sayHello模块头文件和实现文件分离,main.go文件只需要引入.h文件即可。
需要注意的是,这里的编译不能用go build main.go
的指定文件方式编译,这种编译会下面的报错。
# command-line-arguments
/tmp/go-build799889451/b001/_x002.o:在函数‘_cgo_3e94971ce40c_Cfunc_sayHello’中:
/tmp/go-build/cgo-gcc-prolog:61:对‘sayHello’未定义的引用
collect2: 错误:ld 返回 1
在main.go文件的目录下执行go build
编译文件,执行在终端上打印hello world
字样。
//sayHello.h
#ifndef _HELLO_H
#define _HELLO_H
void sayHello(const char *s);
#endif
//sayHello.c
#include
#include "sayHello.h"
void sayHello(const char *s)
{
puts(s);
}
package main
/*
#include "sayHello.h"
#include
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.sayHello(cs)
}
和 go 模块化调用 c 代码类似,调用 c++时同样将头文件和实现文件分离,只不过为了满足 go 调用的 c 的
函数范式,需要在 c++的实现文件中以 c 的风格引入头文件。
#define _HELLO_H_
void sayHello(const char* s);
#endif // !_HELLO_H_
在这个文件中使用 c++的标准输出流输入字符串到终端上。
#include
extern "C"{
#include "hello.h"
}
void sayHello(const char* s)
{
std::cout << s << std::endl;
}
调用部分和之前的一样。
package main
/*
#include"hello.h"
#include
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.sayHello(cs)
}
编译时不能指定文件编译,执行执行go build
即可。
文件结构:
.
├── hello.cpp
├── hello.h
├── main.go
└── README.md
0 directories, 4 files
待续。。。
上面小结介绍了go调用c和c++函数的方式和过程,这小结我们看一下如何将go的函数导出,给c语言的函数使用。
在1.1.3章节实现了一个c函数,用于在终端打印一个字符串。现在我们想用go函数来打印这个字符串,并且将这个函数导出,然后再用go的主程序来调用这个导出函数,实现打印的目的。以下用 go 语言实现hello.h
的函数功能,在 go 的函数中实现 c 语言的调用。
定一个名为hello.go
的文件,在这个文件中实现一个打印字符串到终端的函数,并且使用关键字将这个函数导出。
package main
import "C"
import "fmt"
//export SayHello
func SayHello(s *C.char) {
fmt.Println(C.GoString(s))
}
上面函数的//export SayHello
表示的含义和必须的要求:
//export
为导出的关键子,斜杠后面不能用空格,必须挨着SayHello
为导出的函数名,必须功能函数名字一样,且函数的参数需要转换为 c 包中定义的变量上面的函数在编译时就导出了一个 go 函数,对应的是hello.h
文件中的SayHello
函数。
定一个hello.h
的头文件,在这个文件中声明一个 c 的函数。
void SayHello(/*const*/ char* s);
在main.go
文件中,直接使用导出的函数SayHello
,需要引入hello.h
的头文件。我们只是在hello.h
文件中声明了SayHello
函数,程序在编译和链接时使用的是我们在文件hello.go
文件中实现并导出的SayHello
函数。
package main
/*
#include
*/
import "C"
func main() {
C.SayHello(C.CString("hello world"))
}
.
├── hello.go
├── hello.h
├── main.go
└── README.md
0 directories, 4 files
在main.go
文件下执行go build
不指定文件编译即可。
在上面 1.1 中使用文件分离的方式实现了 go 函数的导出并且调用。这个示例中将在一个文件中导出 go 的函数并且在 go 的main
函数中调用。
package main
/*
#include
void SayHello(char * s);
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.SayHello(cs)
}
//export SayHello
func SayHello(s *C.char) {
fmt.Println(C.GoString(s))
}
上面的代码分为 3 个部分。
这部分声明了一个 c 风格的函数SayHello
,由于在调用时需要释放申请的内存,因此引入了 c 的标准库stdlib.h
文件。
*注意*:第一个/*
到import "C"
之间不能有空行。
/*
#include
void SayHello(char * s);
*/
import "C"
导出名为SayHello
的 go 函数,参数使用 c 包中定义的参数类型。
//export SayHello
func SayHello(s *C.char) {
fmt.Println(C.GoString(s))
}
调用导出函数和之前的一样,用 free 释放内存。
func main() {
cs := C.CString("hello world")
defer C.free(unsafe.Pointer(cs))
C.SayHello(cs)
}
.
└── main.go
0 directories, 1 file
在 main.go 目录下执行go build
直接编译。
在 1.2 章节中的导出函数参数仍然使用的是 c 包中的参数,但是我们能不能使用 go 的参数类型呢?毕竟这是在 go 的函数中呀,我们看如何用 go 的参数实现。
通过分析可以发现 SayHello 函数的参数如果可以直接使用 Go 字符串是最直接的。在 Go1.10 中 CGO 新增加了一个GoString预定义的 C 语言类型,用来表示 Go 语言字符串。下面是改进后的代码:
//build in go1.10
package main
/*
void SayHello(_GoString_ s);
*/
import "C"
import (
"fmt"
)
func main() {
C.SayHello("Hello World")
}
//export SayHello
func SayHello(s string) {
fmt.Println(s)
}
在这个例子中总结了两个方面的知识点:
一共有四个文件,hello.h
、hello.c
分别是c函数的声明和实现,hello.go
为go的导出函数的声明和实现文件,main.go
调用函数。
.
├── hello.c
├── hello.go
├── hello.h
└── main.go
0 directories, 4 files
在这个名为hello.go
的文件中,定义了两个导出函数SayHello
和export_flow
。
SayHello
函数接受一个char的入参,并且将入参打印到终端。
export_flow
函数没有入参和返回值,这个函数被调用后会在终端上打印一个字符串。
package main
import "C"
import "fmt"
//export SayHello
func SayHello(s *C.char) {
fmt.Println(C.GoString(s))
}
//export export_flow
func export_flow() {
// 这个是测试的go的回调函数,这个函数注入到c的代码中,可以理解为在这个函数中实现了数据的处理
fmt.Println("this is flow func in go")
}
这个文件中声明了在文件hello.go
中导出的函数SayHello
,只是在这个位置声明,实现是在go文件中实现的。
另一个声明callFolw
是c语言的函数。这个还是接受了flow
类型的参数fn(指针函数)
//hello.h
#ifndef _HELLO_H_
#define _HELLO_H_
//声明一个回调函数的类型,这个类型名为flow,没有入参,返回值为void
typedef void (*flow)();
//go导出函数的声明
void SayHello(char * s);
// c语言函数的声明
void callFlow(flow fn);
#endif
根据2.4.2和2.4.3,在这个文件中实现了c语言的函数callFlow,只是调用了一个函数指针fn
#include "hello.h"
#include
#include
void callFlow(flow fn)
{
fn();
}
package main
/*
#include "hello.h"
#include
extern void export_flow();
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("Hello World")
defer C.free(unsafe.Pointer(cs))
C.SayHello(cs)
C.callFlow(C.flow(C.export_flow))
}
上面的主程调用中大致可以分为2个部分,依次说明:
/*
#include "hello.h"
#include
extern void export_flow();
*/
import "C"
第2行:引入了.h头文件声明,声明go的导出函数SayHello
、c语言指针函数flow
和c语言函数callFolw
第3行:引入c语言标准库free
来释放内存
第4行:声明一个c语言的导出函数export_flow
,这个函数无入参,返回值为void,对应的是hello.go
文件中的export_flow
函数
第5、6行:必须无空行,且不能合并import "C"
cs := C.CString("Hello World")
defer C.free(unsafe.Pointer(cs))
C.SayHello(cs)
C.callFlow(C.flow(C.export_flow))
第1、2行:定义一个字符串,并申请空间。延迟释放申请到的空间
第3行:调用go导出的函数SayHello
,在终端打印hello world
字样
第4行:
C.callFlow
调用c语言的函数callFlow
C.export_flow
是go的导出函数export_flow
callFlow
函数有一个类型为flow
的指针函数作为入参,我们把go的导出函数export_flow
作为callFlow的入参,此时需要转换一下类型,合并之后就是C.callFlow(C.flow(C.export_flow))
在main函数的目录下运行go build
编译,运行结果:
Hello World
this is flow func in go
将go的函数导出,作为参数传递给c的函数,作为回调函数使用。将c函数中的数据回调到go的代码中做业务逻辑。在这个例子中回调了常见的数据读取方式,一个指针和长度,用于取c函数中的一段内存数据。
.
├── hello.c
├── hello.go
├── hello.h
└── main.go
0 directories, 4 files
hello.h
和hello.c
文件实现c函数的逻辑和部分变量的定义,hello.go
文件定义go的导出函数,main.go
是调用的主函数。
hello.h
声明 在这个文件中,定义了一个函数指针类型export_fetchDatas
,作为c语言函数的回调参数。引用了go的导出函数export_fetchDatas
,extern
关键字说明函数已经在别的文件中(hello.go
)实现,此处只是引用。同时定义了两个c语言的函数login
和logout
作为调用入口。
// hello.h
#ifndef _HELLO_H_
#define _HELLO_H_
//定义用户信息的结构体
typedef struct tagUserInfo
{
unsigned char* username;
unsigned char* password;
}USERINFO,*LUNSERINFO;
//引用go的导出函数
extern int export_fetchDatas(unsigned char *pBuf, unsigned int RevLen);
//函数指针(用于回调)
typedef int (*fetchDatas)(unsigned char*,unsigned int);
// c语言函数的声明
void login(LUNSERINFO userinfo, fetchDatas fn);
void logout();
#endif
实现文件hello.c
, 在实现中使用死循环来一直向入参回调变量中写入数据。
#include "hello.h"
#include
#include
#include
#include
bool _bExit = false;
void login(LUNSERINFO userinfo, fetchDatas fn)
{
printf("<< login@:login msg\n");
if (0 == strcmp(userinfo->username, "admin"))
printf("<< login@: username=%s, password=%s login success.\n:", userinfo->username, userinfo->password);
else
printf("<< login@: username=%s\n", userinfo->username);
char buffer[512];
int count = 0;
while (1)
{
if (_bExit == true)
{
printf("<< login@:fetch exit\n");
break;
}
int n = sprintf(buffer, "count=%d", count++);
fn(buffer, strlen(buffer));
memset(buffer, 0, sizeof(buffer));
sleep(5);
}
printf("<< login@:promt data end.\n");
}
void logout()
{
printf("<< logout@:rcv logout signal\n");
_bExit = true;
}
go的导出函数文件中定义了导出函数export_fetchDatas
,参数直接使用了"C"
包中的定义,这里的参数类型需要与hello.h
文件中export_fetchDatas
和fetchDatas
的类型一致,要不然编译报错。
// hello.go
package main
import "C"
import (
"reflect"
"unsafe"
)
//export export_fetchDatas
func export_fetchDatas(pBuf *C.uchar, RevLen C.uint) int {
var buf []byte
data := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
data.Data = uintptr(unsafe.Pointer(pBuf))
data.Len = int(RevLen)
data.Cap = int(RevLen)
logger.Printf("## export_fetchWithErr@:this is callback result:%v", string(buf))
return 0
}
以下是摘录的部分主程序调用代码。
var userinfo = C.struct_tagUserInfo{
}
userinfo.username = (*C.uchar)(unsafe.Pointer(username))
userinfo.password = (*C.uchar)(unsafe.Pointer(password))
logger.Printf(">> 调用c的函数login,模拟登陆的操作,并传递一个go的到处函数作为login函数的回调,打印c函数中回调得到信息")
C.login((*C.struct_tagUserInfo)(unsafe.Pointer(&userinfo)), C.fetchDatas(C.export_fetchDatas))
编译时,直接在main.go
文件的同级目录下运行go build
即可,运行后hello.go
的导出函数会一致打印收到的c函数中的数据。
在这个例子中,go的导出函数接受一个结构体变量,并且将结构体变量打印到终端上。
这个示例中同样是4个文件,略有不同的是这次使用的是cpp的文件,用c的方式引入定义,cpp中可以用c++实现。
.
├── hello.cpp
├── hello.go
├── hello.h
└── main.go
0 directories, 4 files
这个c的头文件中,定义了一个结构体tagStudent
,这个结构体又包含一个结构体。里面需要注意的一些问题后面集中整理。
// hello.h
#ifndef _HELLO_H_
#define _HELLO_H_
//定义用户信息的结构体
typedef struct tagHomeInfo
{
char addr[255]; //住址
char code[255]; //邮编
} HOMEINFO, *LHOMEINFO;
typedef struct tagStudent
{
HOMEINFO homeInfo; //家庭信息
char name[255]; //姓名
char gender; //性别
int age; //年龄
} STUDENT, *LSTUDENT;
//函数指针(用于回调)
typedef void (*fetch_student)(STUDENT *);
//go导出函数的声明
extern void export_fetch_student(STUDENT *stu);
// c语言函数的声明
void queryStudent(fetch_student fn);
#endif
这个导出函数和2.5中的导出函数不同的是,在go的文件中又定义了一份结构体。这个结构体和在c函数中定义的结构体在结构和字段的类型上一致。c结构体中的子结构在什么位置定义,go中的结构体必须在同样的位置上定义。
go的导出函数必须用c中函数的真实而定结构体名,在这个里面体现为tagStudent
(访问c的结构体需要加struct_
前缀)。导出函数收到c的结构体后指针,将结构体指针强制转换成go的结构体指针。然后就可以使用go的结构体对象了。
这个文件中因为使用到了c函数的定义中结构体,因此也引入了C包和hello.h
头文件。
package main
/*
#include "hello.h"
*/
import "C"
import (
"unsafe"
)
type Student struct {
HomeInfo HomeInfo
Name [255]byte
Gender byte
Age int32
}
type HomeInfo struct {
Addr [255]byte
Code [255]byte
}
//export export_fetch_student
func export_fetch_student(st *C.struct_tagStudent) {
p := (*Student)(unsafe.Pointer(st))
name := p.Name
gender := p.Gender
age := p.Age
addr := p.HomeInfo.Addr
code := p.HomeInfo.Code
logger.Printf("name=%s", string(name[:]))
logger.Printf("gender=%c", gender)
logger.Printf("age=%d", age)
logger.Printf("addr=%s", string(addr[:]))
logger.Printf("code=%s", string(code[:]))
}
调用的主程摘录代码。在需要引入hello.h
头文件。
package main
/*
#include "hello.h"
#include
*/
import "C"
import (
"log"
"os"
)
var logger *log.Logger = nil
func main() {
logger = log.New(os.Stdout, "", log.Lshortfile|log.Ltime|log.Ldate)
logger.Printf(">> main start")
C.queryStudent(C.fetch_student(C.export_fetch_student))
logger.Printf(">> main exit")
}
编译时,在main.go
文件的同级目录下执行go build
即可,运行结果:
c函数的内存稳定,已经申请到的内存在没有人为干预释放时,在go中是可以随便使用的。由于go语言的限制,go中无法申请到大于2GB的切片(参考go的makeslice函数的实现),可以使用c内存的稳定的特征,简介使用c的内存来实现go的大内存的使用。
package main
/*
#include
void* makeslice(size_t memsize) {
return malloc(memsize);
}
*/
import "C"
import "unsafe"
func makeByteSlize(n int) []byte {
p := C.makeslice(C.size_t(n))
return ((*[1 << 31]byte)(p))[0:n:n]
}
func freeByteSlice(p []byte) {
C.free(unsafe.Pointer(&p[0]))
}
func main() {
s := makeByteSlize(1<<32+1)
s[len[s]-1] = 1234
print(s[len[s]-1])
freeByteSlice(p)
}
例子中我们通过makeByteSlize来创建大于4G内存大小的切片,从而绕过了Go语言实现的限制(需要代码验证)。而freeByteSlice辅助函数则用于释放从C语言函数创建的切片。
因为C语言内存空间是稳定的,基于C语言内存构造的切片也是绝对稳定的,不会因为Go语言栈的变化而被移动。
将go的字符串传递给c函数,c函数打印传入的字符串。假设一个极端的场景:我们将一块位于某goroutinue的栈上的Go语言内存传入了C语言函数后,在此C语言函数执行期间,此goroutinue的栈因为空间不足的原因发生了扩展,也就是导致了原来的Go语言内存被移动到了新的位置。但是此时此刻C语言函数并不知道该Go语言内存已经移动了位置,仍然用之前的地址来操作该内存——这将将导致内存越界。以上是一个推论(真实情况有些差异),也就是说C访问传入的Go内存可能是不安全的!
上文中1.1.3就是用到了这种手法,在go中使用C包中的C.CString
函数将go的内存拷贝到申请的c的内存中,在这个过程中go和c的内存均由自己的内存管理方式管理,因此需要手动的释放C.CString
申请的c内存。当用到比较多的字符时会有很多的繁琐操作,很不方便。
为了简化这种方式,cgo提供了更便捷的方式。
package main
/*
void printString(const char* s) {
printf("%s", s);
}
*/
import "C"
func printString(s string) {
C.printString((*C.char)(unsafe.Pointer(&s[0])))
}
func main() {
s := "hello"
printString(s)
}
假设调用的C语言函数需要长时间运行,那么将会导致被他引用的Go语言内存在C语言返回前不能被移动,从而可能间接地导致这个Go内存栈对应的goroutine不能动态伸缩栈内存,也就是可能导致这个goroutine被阻塞。因此,在需要长时间运行的C语言函数(特别是在纯CPU运算之外,还可能因为需要等待其它的资源而需要不确定时间才能完成的函数),需要谨慎处理传入的Go语言内存。
取到go的内存指针后需要立刻传入到c中,防止go的内存发生扩展时,导致内存结构变化。
思路:
go内存由go的内存管理模型管理,c由c的内存管理模型管理,两者中间使用稳定的类型来桥接,达到c长期持有go内存的想法。
实现:
// 假装有代码
go导出函数给c函数用时,不能使用go申请的内存,因为go和c有着截然不同的内存管理模式。
/*
extern int* getGoPtr();
static void Main() {
int* p = getGoPtr();
*p = 42;
}
*/
import "C"
func main() {
C.Main()
}
//export getGoPtr
func getGoPtr() *C.int {
return new(C.int)
}
在运行时会报错,cgo中函数_cgo_tsan_acquire
会扫描内存指针,会检查cgo中是否包含Go的指针,需要说明的是,cgo默认对返回结果的指针的检查是有代价的,特别是cgo函数返回的结果是一个复杂的数据结构时将花费更多的时间。如果已经确保了cgo函数返回的结果是安全的话,可以通过设置环境变量GODEBUG=cgocheck=0来关闭指针检查行为。
$ GODEBUG=cgocheck=0 go run main.go
关闭cgocheck功能后再运行上面的代码就不会出现上面的异常的。但是要注意的是,如果C语言使用期间对应的内存被Go运行时释放了,将会导致更严重的崩溃问题。cgocheck默认的值是1,对应一个简化版本的检测,如果需要完整的检测功能可以将cgocheck设置为2。