Using a Go
calling C
library, tends to be more common than using a C
calling dynamic library ( previous post ). The reason why the public library is encapsulated by the underlying similar language and called by the upper application layer language is either the bottleneck of performance; or the maintenance cost of public services is reduced, especially in a large company, there are multiple languages. The team that uses a language to encapsulate the core functions, and then invoked by the application layer language, often this development mode has lower maintenance costs.Go
C
The application layer and core services are often two separate projects in the actual process, and are maintained by different teams. So the following process is completely standing in this actual scenario to think about C
the common library of language design, how to cgo
integrate it, and then Go
use it in the project. The main steps are as follows:
- contract interface, encapsulate interface
- Generate dynamic library
- Access to use
1. Agreed interface, encapsulated interface
As both parties of the project, the first step must be to agree on the interface function. Here we assume that a public service similar to processing message queue data is designed. The core of this public service is to complete the initial connection ( Init
function) of the message queue, pull data from the consumption queue ( Start
function), and stop the service ( Stop
function), so we put These three methods are exposed to the Go
language and are called by Go
origin .
// 暴露给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);
复制代码
Since the processing of message queue data is a real-time polling process, once the data is read, we need to send the data back to the Go
upper-layer application, so a callback function ( ) is also agreed here sendmsg_callback
.
After agreeing on the function interface, you can go your own way. For the development of the underlying public services, it is only necessary to implement the detailed functions of three functions Init
, , , and . The function needs to be implemented, so use the keyword tag in the declaration.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
For registration, it is a callback object (here is a function, you can also design it as other data types) to pass to C
, C
this will be returned when appropriate id
, and then the id
corresponding data type will be found through this, in fact, this is also the official wiki introduction callback The main idea of the function.
C.Init
The call is directly initialized, C.Start
responsible for starting tasks, and C.Stop
preparing for recycling tasks. Finally compile and run:
# build
go build -o bin/app main.go
# 运行
LD_LIBRARY_PATH=./libs ./bin/app
复制代码
Unsurprisingly, every 5 seconds, the sendmsg_callback
function will receive C
the string data returned by the core library Stop()
until it is displayed and closed. This is the entire usage process.
Notice
In fact, after mastering the entire cgo
development process, the rest is not complicated. But it still needs to be emphasized that the memory destruction problem of pointer type data between the two languages, such as strings, arrays, and even more complex types, must pay attention to the memory release problem. Go
What is passed to C
is C.CString
the space opened up in the heap memory, no matter which party it is, the memory must be released in the end, just like in C
the function implemented by the above Stop()
, we free
dropped all the passed string data.
Exactly, here is also a question for readers. If the underlying library returns a string pointer C
similar to the above when calling back , do we need to manually release this memory?sendmsg_callback
* C.char