工作中遇到的Go语言调用C函数的场景比较多,之前也写过一篇《cgo中将C函数返回的数组转为Go中的slice》。
目前在开发OpenSIPS的过程中,有些功能用C写起来麻烦,故第一次尝试了用C调用Go。
首先用Go实现功能,示例代码如下
package main
import "C"
import (
"fmt"
"sync"
"time"
)
var notifyChan chan struct{} = make(chan struct{})
var wg sync.WaitGroup
//export Test
func Test(strs []string) string {
for _, v := range strs {
fmt.Println(v)
}
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer func() {
fmt.Printf("work %d cleaning\n",idx)
wg.Done()
}()
for {
select {
case <-notifyChan:
fmt.Printf("work %d should exit\n",idx)
return
default:
}
fmt.Printf("work %d working\n",idx)
time.Sleep(time.Second)
}
}(i)
}
return "OK"
}
//export Close
func Close() {
close(notifyChan)
wg.Wait()
}
//export Test2
func Test2(input map[string]string) map[string]string {
return nil
}
func main() {
}
用命令生成动态库和头文件:
go build -o libfunc.so -buildmode=c-shared func.go
有几个注意点:
1. 必须是package main并且包含main函数,否则go build会报错
2. main函数里即使有代码段,也不会执行,所以为空即可
3. "import C" 不要忘记,否则虽然能生成动态库,但里面不会有导出符号,头文件也不会生成
4. export和"//"之间不能有空格
生成的头文件:
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package command-line-arguments */
#line 1 "cgo-builtin-export-prolog"
#include <stddef.h> /* for ptrdiff_t below */
#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif
#endif
/* Start of preamble from import "C" comments. */
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#line 1 "cgo-gcc-export-header-prolog"
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern GoString Test(GoSlice strs);
extern void Close();
extern GoMap Test2(GoMap input);
#ifdef __cplusplus
}
#endif
可以看到,Go函数的切片在头文件中是GoSlice类型,包含指向实际数据的指针、数据的长度和slice的容量。切片中元素的类型是GoString,包含一个指向字符串地址的指针和字符串的长度。
注意:GoString是返回类型的时候,const char* p指向的字符串不是以尾0结束的,需要根据长度n自己拷贝使用!
C语言调用的示例代码如下
#include "stdio.h"
#include "memory.h"
#include "stdlib.h"
#include "malloc.h"
#include "string.h"
#include "libfunc.h"
int main()
{
GoString sa[2] = {{"hello",strlen("hello")},{"world",strlen("world")}};
GoSlice strs;
strs.data = sa;
strs.len = 2;
strs.cap = 2;
GoString ret = Test(strs);
printf("ret(%d) is: %.*s\n",ret.n,ret.n,ret.p);
//printf("ret: %s\n",ret.p);
sleep(10);
Close();
printf("exit.\n");
return 0;
}
可以看到打印Test返回结果的时候,要用"%.*s"打印,用"%s"的话会打出来一些古怪的东西。
有两点说明的地方:
1. C语言main退出后,会直接结束掉Go里的所有线程,defer得不到执行(这其实在go里也是一样),一般可以设计一个Close函数做清理工作。
2. 对于Go的map类型,头文件中是void*,是没办法在C中构造一个Go的map传给动态库的,也没法从动态库获取一个Go的map。有一些workaround,比如把map转为json字符串,或者传递两个数组分别为key和value。