NanoPB: C++ でオフセットとサイズを持つカスタム配列コンバータ

関連記事: C 版: C でカスタム配列コンバータを扱う方法

NanoPB は組み込みシステム向けにコードサイズを最適化した Protocol Buffers 実装です。この記事では、KKS-Firmware の実装に基づいて、NanoPB で C++ のカスタム配列コンバータを扱う方法を説明します。

Proto 定義

まず、リピートフィールドを持つ .proto ファイルを作成します。

custom_array.proto
syntax = "proto3";

package example;

message CustomArrayMessage {
  repeated uint32 values = 1;
}

NanoPB コードの生成

最大カウントを指定する .options ファイルを使って NanoPB コードを生成します。

custom_array.options を作成します。

custom_array.options
example.CustomArrayMessage.values max_count:20

次に生成します。

generate_nanopb_custom_array.sh
protoc --nanopb_out=. custom_array.proto

これにより custom_array.pb.h と custom_array.pb.c が生成されます。

カスタム配列コンバータを使った C++ の例

オフセットとサイズを持つカスタム配列コンバータを実装した完全な C++ の例を示します。

custom_array_example.cpp
#include <stdio.h>
#include <algorithm>
#include <array>
#include <limits>
#include "custom_array.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"

// オフセットとサイズのテンプレートパラメータを持つカスタム配列コンバータ
template<class ITEM_CONVERTER, class CONTAINER, size_t OFFSET=0, size_t SIZE=std::numeric_limits<size_t>::max()/2>
class ArrayConverterWithOffsetAndSize {
public:
    static bool encodeCallback(pb_ostream_t *stream, const pb_field_t *field, const CONTAINER &container) {
        size_t i = 0;
        // for ループでコンテナを反復処理
        for (const auto &item : container) {
            i++; // 最初にチェックするインデックスは1
            if(i <= OFFSET) { // "<" ではなく "<="(この行の前で ++ しているため)
                continue;
            }
            if(i > OFFSET + SIZE) {
                break;
            }
            // アイテムをエンコード
            if (!ITEM_CONVERTER::encodeCallback(stream, field, item)) {
                return false;
            }
        }
        return true;
    }

    static bool decodeCallback(pb_istream_t *stream, const pb_field_t *field, CONTAINER &container) {
        // コンテナにアイテムを追加
        // 注意: これは簡略化した例 - 実際の実装はコンテナ型に依存
        return false; // この例では未実装
    }
};

// シンプルな uint32 コンバータ
class UInt32Converter {
public:
    static bool encodeCallback(pb_ostream_t *stream, const pb_field_t *field, uint32_t value) {
        if (!pb_encode_tag_for_field(stream, field))
            return false;
        return pb_encode_varint(stream, value);
    }
};

int main() {
    // エンコードされたメッセージ用バッファ
    uint8_t buffer[256];
    size_t message_length;
    
    // 10個の要素を持つ配列を作成
    std::array<uint32_t, 10> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // --- エンコード ---
    example_CustomArrayMessage message = example_CustomArrayMessage_init_zero;
    
    // カスタムコンバータを使用: 要素3-7をエンコード(OFFSET=2、SIZE=5)
    using CustomConverter = ArrayConverterWithOffsetAndSize<UInt32Converter, std::array<uint32_t, 10>, 2, 5>;
    
    // コールバックを設定
    message.values.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
        const std::array<uint32_t, 10>* container = (const std::array<uint32_t, 10>*)*arg;
        return CustomConverter::encodeCallback(stream, field, *container);
    };
    message.values.arg = &values;
    
    // エンコード用ストリームを作成
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    // メッセージをエンコード
    if (!pb_encode(&ostream, example_CustomArrayMessage_fields, &message)) {
        printf("Encoding failed: %s\n", PB_GET_ERROR(&ostream));
        return 1;
    }
    
    message_length = ostream.bytes_written;
    printf("Encoded %zu bytes (elements 3-7)\n", message_length);
    
    // 16進ダンプを出力
    printf("Encoded data: ");
    for (size_t i = 0; i < message_length; i++) {
        printf("%02x ", buffer[i]);
    }
    printf("\n");
    
    // エンコードされた内容を出力
    printf("Encoded values: ");
    for (size_t i = 2; i < 2 + 5 && i < values.size(); i++) {
        printf("%u ", values[i]);
    }
    printf("\n");
    
    return 0;
}

コンパイルコマンド

nanopb を使って例をコンパイルします。NanoPB は通常、ソースファイルをプロジェクトに直接含めて使用します。

compile_custom_array_example.sh
g++ -o custom_array_example custom_array_example.cpp custom_array.pb.c pb_common.c pb_encode.c pb_decode.c -I.

注意: NanoPB のソースファイル(pb_common.c、pb_encode.c、pb_decode.c)は、プロジェクトと一緒に直接コンパイルする必要があります。これらは NanoPB GitHub リポジトリ から入手できます。

ポイント

  • テンプレートパラメータ: OFFSET と SIZE がどの要素をエンコードするかを制御します
  • オフセットロジック: 最初の OFFSET 個の要素をスキップします(ループ内は1ベースのインデックス)
  • サイズ制限: オフセット以降の最大 SIZE 個の要素をエンコードします
  • コールバックパターン: カスタムエンコード/デコード関数で pb_callback_t を使用します
  • 柔軟性: コピーせずに配列の任意のスライスをエンコードできます
  • メモリ効率: 一時配列を作成する必要がありません
  • KKS-Firmware パターン: KKS-Firmware の ArrayConverterWithOffsetAndSize に基づいています

カスタム配列コンバータを使う場面

  • 大きな配列の部分集合をエンコードする必要がある場合
  • データのコピーを避けたい場合
  • ページネーションやチャンキングを実装する場合
  • 配列レイアウトが protobuf の要件と一致しない場合
  • 配列にカスタムエンコードロジックが必要な場合

期待される出力

custom_array_expected_output.txt
Encoded 10 bytes (elements 3-7)
Encoded data: 08 03 08 04 08 05 08 06 08 07 
Encoded values: 3 4 5 6 7 

要素 3、4、5、6、7 のみがエンコードされていることに注目してください(オフセット2から5つの要素)。

応用: デコードのサポート

デコードをサポートするには、コンバータを拡張します。

decode_callback_snippet.cpp
static bool decodeCallback(pb_istream_t *stream, const pb_field_t *field, CONTAINER &container) {
    uint64_t value;
    if (!pb_decode_varint(stream, &value))
        return false;
    
    // コンテナに追加(実装はコンテナ型に依存)
    // std::array の場合、別のインデックスを追跡する必要があるかもしれません
    return true;
}

実際の使用例

このパターンは KKS-Firmware で以下のために使用されています。

  • 大きなチャネル配列からアクティブなチャネルのみをエンコード
  • 帯域を節約するために部分的なセンサーデータを送信
  • 差分更新の実装
  • スパース配列の効率的な処理

その他の NanoPB 記事


同じカテゴリの類似記事をチェック: 組み込み C/C++ Protocol Buffers