cgoプラクティス2:GoプロジェクトでのCパッケージのダイナミックライブラリへのアクセス

Go呼び出し元のライブラリを使用することは、呼び出し側のダイナミックライブラリを使用するよりも一般的Cである傾向があります(前の投稿)。パブリックライブラリが基盤となる類似言語によってカプセル化され、上位のアプリケーション層言語によって呼び出される理由は、パフォーマンスのボトルネックであるか、特に大企業では複数の言語が存在するため、パブリックサービスのメンテナンスコストが削減されます。言語を使用してコア機能をカプセル化し、アプリケーション層言語によって呼び出されるチームでは、多くの場合、この開発モードの方が保守コストが低くなります。CGoC

スクリーンショット2022-03-2712.11.41pm.png

多くの場合、アプリケーション層とコアサービスは、実際のプロセスでは2つの別個のプロジェクトであり、異なるチームによって維持されます。したがって、次のプロセスは、この実際のシナリオで完全にC機能し、言語設計の共通ライブラリ、cgoそれを統合する方法、およびGoプロジェクトで使用する方法について考えます。主な手順は次のとおりです。

  1. コントラクトインターフェイス、カプセル化インターフェイス
  2. ダイナミックライブラリを生成する
  3. 使用するためのアクセス

1.合意されたインターフェース、カプセル化されたインターフェース

プロジェクトの両当事者として、最初のステップはインターフェース機能について合意することでなければなりません。ここでは、メッセージキューデータの処理と同様のパブリックサービスが設計されていると仮定します。このパブリックサービスのコアはInit、メッセージキューの初期接続(機能)を完了し、消費キュー(Start機能)からデータをプルして、サービスを停止することです。 (Stop関数)なので、これらの3つのメソッドはGo言語に公開され、 Goorigin。

// 暴露给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

関数インターフェースに同意した後、あなたはあなた自身の道を進むことができます。基盤となる公共サービスの開発には、3つの機能、、、、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対応するデータ型が実際に検出されます。 、これは公式のwiki紹介コールバックでもあります関数の主なアイデア。

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、残りは複雑ではありません。ただし、文字列、配列、さらに複雑な型など、2つの言語間のポインタ型データのメモリ破壊の問題は、メモリ解放の問題に注意を払う必要があることを強調する必要があります。Go渡されるのCC.CStringヒープメモリで開かれたスペースです。どちらのパーティであっても、上記C実装された関数と同様に、最後にメモリを解放する必要があります。渡された文字列データをすべて削除しました。Stop()free

正確には、ここにも読者への質問があります。基になるライブラリがCコールバック時に上記と同様sendmsg_callbackの文字列ポインタを返す場合、* C.charこのメモリを手動で解放する必要がありますか?

おすすめ

転載: juejin.im/post/7079628114258575396