cgo 사례 2: Go 프로젝트의 C 패키지 동적 라이브러리에 대한 액세스

Go호출 라이브러리를 사용 하는 것은 호출하는 동적 라이브러리를 사용하는 것보다 더 일반적인 C경향이 있습니다 ( 이전 게시물 ). 공용 라이브러리가 기본 유사 언어에 의해 캡슐화되고 상위 응용 계층 언어에 의해 호출되는 이유는 성능의 병목 현상이거나 공공 서비스의 유지 관리 비용이 감소하거나 특히 대기업의 경우 여러 언어가 있기 때문입니다. 핵심 기능을 캡슐화하기 위해 언어를 사용하고 애플리케이션 계층 언어에 의해 호출되는 팀에서 이 개발 모드는 유지 관리 비용이 더 낮습니다.CGoC

스크린샷 2022-03-27 12.11.41 pm.png

애플리케이션 계층과 핵심 서비스는 실제 프로세스에서 두 개의 별도 프로젝트인 경우가 많으며 서로 다른 팀에서 유지 관리합니다. 따라서 다음 프로세스는이 실제 시나리오에서 완전히 서서 C언어 디자인의 공통 라이브러리, cgo통합 방법 및 Go프로젝트에서 사용하는 방법에 대해 생각합니다. 주요 단계는 다음과 같습니다.

  1. 계약 인터페이스, 캡슐화 인터페이스
  2. 동적 라이브러리 생성
  3. 사용 액세스

1. 합의된 인터페이스, 캡슐화된 인터페이스

프로젝트의 양 당사자로서 첫 번째 단계는 인터페이스 기능에 동의하는 것입니다. 여기서는 메시지 큐 데이터를 처리하는 것과 유사한 공용 서비스가 설계되었다고 가정합니다. 이 공용 서비스의 핵심은 메시지 큐의 초기 연결( Init함수)을 완료하고 소비 큐( Start함수)에서 데이터를 가져오고 서비스를 중지하는 것입니다. ( Stop함수), 그래서 우리는 이 세 가지 메소드를 언어에 노출하고 originGo 에 의해 호출합니다 .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()Goextern

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开发整理流程是怎样的。上述代码大致实现内容是:

  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등록을 위해 에 전달할 콜백 객체(여기에는 함수가 있습니다. 다른 데이터 유형으로 설계할 수도 있습니다) 에 전달하고 C, C적절한 경우 이것이 반환 id되고, id실제로 이를 통해 해당 데이터 유형을 찾을 수 있습니다. , 이것은 공식 위키 소개 콜백이기도 합니다. 함수의 주요 아이디어입니다.

C.Init호출은 직접 초기화되어 C.Start작업 시작 및 C.Stop재활용 작업 준비를 담당합니다. 마지막으로 컴파일하고 실행합니다.

# build
go build -o bin/app main.go

# 运行
LD_LIBRARY_PATH=./libs ./bin/app 
复制代码

당연하게도, 함수는 표시되고 닫힐 때까지 5초마다 코어 라이브러리에서 반환된 문자열 데이터 sendmsg_callback를 수신 합니다 . 이것이 전체 사용 프로세스입니다.CStop()

알아 채다

사실, 전체 cgo개발 과정을 마스터하고 나면 나머지는 복잡하지 않습니다. 그러나 문자열, 배열 및 훨씬 더 복잡한 유형과 같은 두 언어 간의 포인터 유형 데이터의 메모리 파괴 문제는 메모리 해제 문제에주의를 기울여야한다는 점을 여전히 강조해야합니다. Go전달되는 C것은 C.CString힙 메모리에서 열린 공간으로, 당사자가 누구이든 간에 결국 메모리를 해제해야 합니다 . 위에서 C구현한 함수 와 마찬가지로 전달된 문자열 데이터를 모두 삭제했습니다 Stop().free

정확히, 여기에 독자를 위한 질문이 있습니다. 기본 라이브러리가 C를 호출할 때 위와 유사한 문자열 포인터를 sendmsg_callback반환하면 * C.char이 메모리를 수동으로 해제해야 합니까?

추천

출처juejin.im/post/7079628114258575396