cgo实践二:Go项目中接入C封装的动态库

使用Go调用C库,往往比使用C调用Go的动态库更为常见(上一篇文章)。之所以很过公共库是由底层类似C语言来封装,由上层应用层语言来调用,要么是性能的瓶颈;要么是降低公共服务的维护成本,尤其是在一家大型公司里,存在着多种语系的团队,使用一种语言来封装核心功能,然后由应用层语言来调用,往往这种开发模式维护成本也较低一些。

截屏2022-03-27 下午12.11.41.png

应用层和核心服务,在实际过程中往往是分开的两个项目,由不通的团队来维护。所以下面的流程完全是站在这种实际的场景中来思考C语言设计的公共库,如何和cgo集成,然后再Go项目中使用。其主要步骤如下:

  1. 约定接口,封装接口
  2. 生成动态库
  3. 接入使用

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)。

约定好函数接口后,然后各行其是。对于开发底层公共服务来说,只需要实现InitStartStop三个函数细节功能即可。而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开发整理流程是怎样的。上述代码大致实现内容是:

扫描二维码关注公众号,回复: 13747055 查看本文章
  1. Init 函数负责连接队列函数,Go来调用,会把请求消息队列的IP、需要操作的topic、以及有数据时的回调函数ID传递过来了。
  2. Start 函数负责来运行这个服务,这里我们开启一个子线程,模拟从消息队列不间断的拉取数据,如果有数据,我们就调用sendmsg_callback,回传给Go语言。
  3. 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 用于注册是一个回调对象(这里是个函数,你也可以设计为其它数据类型)传递给CC在适当的时候会再回传这个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 传递给CC.CString是在堆内存开辟的空间,不管是哪一方,最后一定需要释放这些内存,就像是上述由C实现的Stop()函数中,我们free掉了所有传递过来的字符串数据。

正好,这里也留给读者一个问题,如果C底层库,回调时类似上述的sendmsg_callback回传了一个* C.char的字符串指针,我们是否需要手动释放这个内存呢?

猜你喜欢

转载自juejin.im/post/7079628114258575396