使用Go
调用C
库,往往比使用C
调用Go
的动态库更为常见(上一篇文章)。之所以很过公共库是由底层类似C
语言来封装,由上层应用层语言来调用,要么是性能的瓶颈;要么是降低公共服务的维护成本,尤其是在一家大型公司里,存在着多种语系的团队,使用一种语言来封装核心功能,然后由应用层语言来调用,往往这种开发模式维护成本也较低一些。
应用层和核心服务,在实际过程中往往是分开的两个项目,由不通的团队来维护。所以下面的流程完全是站在这种实际的场景中来思考C
语言设计的公共库,如何和cgo
集成,然后再Go
项目中使用。其主要步骤如下:
- 约定接口,封装接口
- 生成动态库
- 接入使用
1. 约定接口,封装接口
作为项目的双方,第一步肯定是约定接口函数。这里我们假定设计了一个类似处理消息队列数据的公共服务,这个公共服务核心是完成消息队列的初始化连接(Init
函数)、从消费队列拉取数据(Start
函数)、停止这个服务(Stop
函数),所以我们把这三个方法暴露给Go
语言,由Go
来调用。
// 暴露给Go使用的接口
void Init(char *host, char *topic, uintptr_t callback);
int Start();
void Stop();
// cgo约定的接口,C -> Go 回传数据使用
extern void sendmsg_callback(uintptr_t callback, char *content);
复制代码
由于在处理消息队列数据是一个实时轮询过程,一旦读取到数据,我们需要把数据回传给Go
上层应用,所以这里还约定了一个回调函数(sendmsg_callback
)。
约定好函数接口后,然后各行其是。对于开发底层公共服务来说,只需要实现Init
、Start
、Stop
三个函数细节功能即可。而sendmsg_callback()
函数则需要Go
需要实现的,所以声明中使用extern
关键字标记即可。
1.1 封装C公共服务
这一步来说,其实比较独立,按照需求实现逻辑即可。所以对于项目头文件结构以及代码逻辑其实都没要求,只是要求最终导出为.so
动态库即可,当然最低要求肯定需要包含上面四个接口函数。
# 项目目录
libkafka/
├── libkafka.h
└── libkafka.c
复制代码
这里定义一个简单的项目工程目录(如上),libkafka.h
头文件是我们自己来设计的,只要包含上述的四个函数即可。甚至你都不需要头文件。直接写在.c
文件也是没问题。其实记住一点,就是开发这个公共并不需要纠结最终这个函数怎么集成到Go
调用方,大家只需要按照接口约定来实现就行。因为很多同学刚开始使用cgo
时,总是在纠结到底如何把自己已经封装好的库集成到Go
项目中去,其实不用太关心,这些都是由些Go
的同学来完成。
libkafka.h声明
#ifndef LIB_KAFKA
#define LIB_KAFKA
#include <stdint.h>
void Init(char *host, char *topic, uintptr_t callback);
int Start();
void Stop();
extern void sendmsg_callback(uintptr_t callback, char *content);
#endif
复制代码
libkafka.c代码实现
//libkafka.c
#include <strings.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include "libkafka.h"
typedef struct {
char *host;
char *topic;
uintptr_t callback;
} kafka_t;
typedef struct {
uintptr_t callback;
} poll_param_t;
kafka_t *kafka;
pthread_t pid;
poll_param_t param;
int cancel;
void *poll(void *args) {
pthread_detach(pthread_self());
// ... 模拟读取队列数据
while (!cancel) {
sleep(5); // 模拟数据, 每隔5秒处理一批数据
// ... 返回数据给 Go回调函数
char * msg = malloc(sizeof(char) * 6);
strcpy(msg, "hello");
printf(" trigger callback: %s, call func: %ld \n", msg, kafka->callback);
sendmsg_callback(kafka->callback, msg);
}
return NULL;
}
void Init(char *host, char *topic, uintptr_t callback) {
kafka = (kafka_t *) malloc(sizeof(kafka_t));
kafka->host = host;
kafka->topic = topic;
kafka->callback = callback;
printf("Init: host=%s, topic=%s, callback=%ld \n", host, topic, callback);
}
int Start() {
printf("Start func \n");
pthread_create(&pid, NULL, &poll, NULL);
return 0;
}
void Stop() {
if (kafka != NULL) { // 释放堆内存的Go分配的字符串内容
free(kafka->host);
free(kafka->topic);
free(kafka);
}
cancel = 1;
// 回收线程
pthread_join(pid, NULL);
printf("stop finished! \n");
}
复制代码
这里我们并没有真正实现这个服务逻辑,而是大致模拟这个服务功能,因为目标是主要是想了解和cgo
开发整理流程是怎样的。上述代码大致实现内容是:
Init
函数负责连接队列函数,Go
来调用,会把请求消息队列的IP
、需要操作的topic
、以及有数据时的回调函数ID
传递过来了。Start
函数负责来运行这个服务,这里我们开启一个子线程,模拟从消息队列不间断的拉取数据,如果有数据,我们就调用sendmsg_callback
,回传给Go
语言。Stop
函数负责来处理停服时的一些回收工作。尤其是,Go
语言带过来的字符串,指针类型数据,一定要free
掉!!!
2. 导出动态库
生成动态库,这里我们还是使用gcc,这里我们生成一个名叫:libkafka.so
的动态库:
gcc -lpthread -fPIC -shared libkafka.c -o libkafka.so
复制代码
3. 接入使用
有了libkafka.so
这个动态库,准备工作算是做好了。剩下的便是接入方需要做的事情。
作为应用层Go
语言来说,需要做点工作,假设我们有个项目需要使用这个公共服务,我们只需要引入这个.so
文件调用里面约定的接口方法即可。我们需要编写一些cgo
的代码:
package main
/*
#include <stdint.h>
#include <stdlib.h>
#cgo LDFLAGS: -L ./libs -lkafka
extern void Init(char *host, char *topic, uintptr_t callback);
extern int Start();
extern void Stop();
*/
import "C"
复制代码
首先来说,如果你没有单独写一个.h
头文件,那么就需要把上面约定的函数接口定义在cgo
注释代码中,约定的函数接口需要使用extern
关键字修饰。而且如果使用了C
相关的数据类型,相关的include
头文件是必不可少。
而且这里我们把 libkafka.so
放在了当前项目的 libs/
下,所以又声明了编译链接包查找路径为-L ./libs -lkafka
,当然你也可以直接放在系统的目录下,省去了这一步。
其外,第二步我们也说了,还有一个sendmsg_callback()
函数是约定的回调函数,是Go
暴露的接口,所以这个需要Go
语言来定义和实现:
//export sendmsg_callback
func sendmsg_callback(callback C.uintptr_t, content *C.char) {
callFunc := cgo.Handle(callback).Value().(func(content string))
callFunc(C.GoString(content))
C.free(unsafe.Pointer(content))
}
func GoCallback(content string) {
fmt.Println("Go Callback String", content)
}
复制代码
在 sendmsg_callback
我们直接通过当前回调函数索引,查找在Init
时传递的Go
类型(这里是个函数),然后再执行这个函数,这里会再触发执行GoCallback()
这个函数。其实在实际中,如果你和C
之前回调仅仅是一对一的,那么其实没必要使用GoCallback
函数,主体逻辑直接可以在sendmsg_callback
完成。这里主要是为了演示服务多场景下接入不同回调才这么做的。
最后,就是直接使用的main
函数入口库代码了:
func main() {
callback := cgo.NewHandle(GoCallback)
C.Init(C.CString("127.0.0.1:9093"), C.CString("queue_test"), C.uintptr_t(callback))
C.Start()
time.Sleep(time.Second * 10)
println("准备停止")
C.Stop()
}
复制代码
cgo.NewHandle
用于注册是一个回调对象(这里是个函数,你也可以设计为其它数据类型)传递给C
,C
在适当的时候会再回传这个id
,然后再通过这个id
找到对应数据类型,其实这也是官方wiki介绍回调函数主要思路。
C.Init
调用直接进行初始化、C.Start
负责启动任务、C.Stop
准备回收任务。最后编译运行:
# build
go build -o bin/app main.go
# 运行
LD_LIBRARY_PATH=./libs ./bin/app
复制代码
不出意外,每隔5秒,sendmsg_callback
函数会收到由C
核心库返回的字符串数据,知道被Stop()
显示的关闭为止,这就是整个使用流程。
注意
其实掌握了整个cgo
开发的流程,剩下的事并不算复杂。但还是需要强调的是两种语言互传指针类型数据的内存销毁问题,如字符串,数组,甚至更复杂的类型时一定要注意内存释放问题。Go
传递给C
的C.CString
是在堆内存开辟的空间,不管是哪一方,最后一定需要释放这些内存,就像是上述由C
实现的Stop()
函数中,我们free
掉了所有传递过来的字符串数据。
正好,这里也留给读者一个问题,如果C
底层库,回调时类似上述的sendmsg_callback
回传了一个* C.char
的字符串指针,我们是否需要手动释放这个内存呢?