Cによる独自エレメント開発

Device Connector Frameworkでは、RustまたはCで独自エレメントを開発し、パイプラインに組み込むことができます。 ここでは、Cインターフェイスを使って独自エレメントを開発する方法を説明します。

Cで開発する場合は、独自エレメントはプラグインとして作成します。

準備

Cによる開発を行うためには、以下の2つのファイルが必要です。

  • Device Connector Frameworkのリポジトリに含まれる インクルードファイル common/include/device_connector.h

  • 静的リンク用のライブラリファイル libdevice_connector_common.a 。以下のコマンドによりビルドしてください。

    git clone https://github.com/aptpod/device-connector-framework.git
    cd device-connector-framework
    cargo build -p device-connector-common --release
    

    このコマンドにより、 target/release 以下に libdevice_connector_common.a が生成されます。

    これを任意のディレクトリにコピーしてください。

    cp target/release/libdevice_connector_common.a </path/to/library_dir>
    

エレメントを実装する

例として "example-plugin" というsrcエレメントを実装します。 このエレメントは設定を持たず、1秒間隔で hello, world from plugin という文字列のメッセージを送信します。

以下のコードブロックを example_plugin.c という名前のファイルとして保存してください。

#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <device_connector.h>

#define N_ELEMENT 1
#define PLUGIN_NAME "example-plugin"

typedef struct {
    const char* text;
} ExamplePlugin;

void *example_plugin_new(const char *config);
DcElementResult example_plugin_next(void* element, DcPipeline *pipeline, DcMsgReceiver *msg_receiver);
bool example_plugin_finalizer(void *element, struct DcFinalizer *finalizer);
void example_plugin_free(void* element);

bool dc_load(DcPlugin *plugin) {
    dc_init(PLUGIN_NAME);

    plugin->version = "0.1.0";
    plugin->n_element = N_ELEMENT;

    DcElement *elements = (DcElement *)malloc(sizeof(DcElement) * N_ELEMENT);

    // Element settings
    elements[0].name = PLUGIN_NAME;
    elements[0].recv_ports = 0;
    elements[0].send_ports = 1;
    elements[0].acceptable_msg_types = NULL;
    elements[0].config_format = "json";
    elements[0].new_ = example_plugin_new;
    elements[0].next = example_plugin_next;
    elements[0].finalizer = example_plugin_finalizer;
    elements[0].free = example_plugin_free;

    plugin->elements = elements;
    return true;
}

void *example_plugin_new(const char *config) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)malloc(sizeof(ExamplePlugin));
    example_plugin->text = "hello, world from plugin";
    return example_plugin;
}

DcElementResult example_plugin_next(void* element, DcPipeline *pipeline, DcMsgReceiver *msg_receiver) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)element;

    if (!dc_pipeline_send_msg_type_checked(pipeline)) {
     DcMsgType msg_type;
     if (dc_msg_type_new("mime:text/plain", &msg_type)) {
         dc_pipeline_check_send_msg_type(pipeline, 0, msg_type);
     }
    }

    sleep(1);

    DcMsgBuf *msg_buf = dc_pipeline_msg_buf(pipeline);

    const uint8_t *data = (const uint8_t *)example_plugin->text;
    const size_t len = strlen(example_plugin->text);
    dc_msg_buf_write(msg_buf, data, len);

    return DcElementResult_MsgBuf;
}

bool example_plugin_finalizer(void *element, struct DcFinalizer *finalizer) {
    return true;
}

void example_plugin_free(void* element) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)element;
    free(example_plugin);
}

以下、このソースコードについて解説していきます。

#include <device_connector.h>

プラグイン開発のために device_connector.h をインクルードします。

typedef struct {
    const char* text;
} ExamplePlugin;

エレメントの本体を定義します。

bool dc_load(DcPlugin *plugin) {
    dc_init(PLUGIN_NAME);

プラグインをロードする時に呼び出される dc_load 関数を定義します。最初に、 dc_init にプラグインの名称を渡します。

plugin->version = "0.1.0";
plugin->n_element = N_ELEMENT;

このプラグインがターゲットとするデバイスコネクターのバージョンと、読み込ませたいエレメントの数を指定します。

DcElement *elements = (DcElement *)malloc(sizeof(DcElement) * N_ELEMENT);

DcElement の配列を malloc で用意します。この長さは読み込ませたいエレメントの数(N_ELEMENT)と同じです。

elements[0].name = PLUGIN_NAME;          // エレメントの名前(実行時に他のエレメントと重複しているとエラーになる)
elements[0].recv_ports = 0;              // 受信用ポートの数
elements[0].send_ports = 1;              // 送信用ポートの数
elements[0].acceptable_msg_types = NULL; // 受け取ることのできるデータ型。何も受信しない場合はNULL
elements[0].config_format = "json";      // 設定フォーマット(json or yaml)。このフォーマットがnew_に指定した関数に渡される
elements[0].new_ = example_plugin_new;   // エレメントを生成する関数
elements[0].next = example_plugin_next;  // エレメントを実行する関数
elements[0].finalizer = example_plugin_finalizer;  // プロセス終了時呼び出されるファイナライザを設定する関数
elements[0].free = example_plugin_free;  // エレメントを終了・解放する関数

エレメントの詳細を定義します。

    plugin->elements = elements;
    return true;
}

plugin->elements にDcElementの配列を設定し、 dc_load が成功したら true を返します。

void *example_plugin_new(const char *config) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)malloc(sizeof(ExamplePlugin));
    example_plugin->text = "hello, world from plugin";
    return example_plugin;
}

エレメントを生成する関数です。 config には、パイプライン設定ファイルに記述されたのエレメントの設定( conf フィールドの値)が、 config_format で指定したフォーマットに変換され文字列として渡されます。(この例では config の値は使用していません。)

ExamplePlugin のための領域を malloc で確保し、初期化後に void ポインタとして返します。失敗時には NULL を返却します。

DcElementResult example_plugin_next(void* element, DcPipeline *pipeline, DcMsgReceiver *msg_receiver) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)element;

example_plugin_next はエレメントを実行するための関数です。 受け取った elementExamplePlugin * にキャストします。

if (!dc_pipeline_send_msg_type_checked(pipeline)) {
    DcMsgType msg_type;
    if (dc_msg_type_new("mime:text/plain", &msg_type)) {
        dc_pipeline_check_send_msg_type(pipeline, 0, msg_type);
    }
}

dc_pipeline_send_msg_type_checked() で送信するメッセージの型チェックが行われているか調べ、行われていなければ、 DcMsgType を作成して dc_pipeline_check_send_msg_type() に渡します。

sleep(1);

DcMsgBuf *msg_buf = dc_pipeline_msg_buf(pipeline);

const uint8_t *data = (const uint8_t *)example_plugin->text;
const size_t len = strlen(example_plugin->text);
dc_msg_buf_write(msg_buf, data, len);

1秒間スリープした後、 msg_buf を取得し、送信したいデータを dc_msg_buf_write() で書き込みます。 ここで書き込むのは example_plugin_new で設定したテキストです。

    return DcElementResult_MsgBuf;
}

DcElementResult_MsgBuf を返し、 msg_buf に書き込んだデータを送信することを示します。

bool example_plugin_finalizer(void *element, struct DcFinalizer *finalizer) {
    return true;
}

プロセス終了時に呼び出されるファイナライザを登録するための関数です。ここでは特に何も行いませんが、プロセス終了時にエレメントが占有するリソースを解放する必要がある場合、ファイナライザに記述します。

void example_plugin_free(void* element) {
    ExamplePlugin *example_plugin = (ExamplePlugin *)element;
    free(example_plugin);
}

終了処理を記述します。ここでは malloc() で確保した領域を free() に渡すだけです。

コンパイルする

上記の example_plugin.c を、GCCでコンパイルするには以下のコマンドを実行します。

gcc -I/include_dir -Wall -O2 -fPIC -shared -L/library_dir \
    -o libdc_example_plugin.so example_plugin.c -ldevice_connector_common

-I オプションで、 device_connector.h ファイルが含まれるディレクトリを、 -L オプションで、 libdevice_connector_common.a ファイルが含まれるディレクトリを指定してください。

これにより、 プラグインファイル libdc_example_plugin.so が生成されます。 使用するには、パイプライン設定ファイルの plugin.plugin_files で、プラグインファイル名を指定してください()。