NanoPB: Vlastní konvertory polí s offsetem a velikostí v C

Viz také: Verze pro C++: Jak pracovat s vlastními konvertory polí v C++

NanoPB je implementace Protocol Buffers optimalizovaná na velikost kódu pro vestavěné systémy. Tento příspěvek ukazuje, jak pracovat s vlastními konvertory polí v C s NanoPB.

Definice Proto

Nejprve vytvořte soubor .proto s opakovanými poli:

custom_array.proto
syntax = "proto3";

package example;

message CustomArrayMessage {
  repeated uint32 values = 1;
}

Generování NanoPB kódu

Vygenerujte kód NanoPB se souborem .options pro určení maximálního počtu:

Vytvořte custom_array.options:

custom_array.options
example.CustomArrayMessage.values max_count:20

Poté vygenerujte:

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

Tím se vygenerují soubory custom_array.pb.h a custom_array.pb.c.

Příklad v C s vlastním konvertorem polí

Zde je kompletní příklad v C implementující vlastní konvertor polí s offsetem a velikostí:

custom_array_example.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <limits.h>
#include "custom_array.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"

// Kontext vlastního konvertoru polí
typedef struct {
    const uint32_t* data;
    size_t size;
    size_t offset;
    size_t count;
} array_context_t;

// Callback kodéru pro pole uint32 s offsetem a velikostí
bool uint32_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
    const array_context_t* ctx = (const array_context_t*)*arg;
    
    size_t start = ctx->offset;
    size_t end = (ctx->offset + ctx->count < ctx->size) ? 
                 ctx->offset + ctx->count : ctx->size;
    
    for (size_t i = start; i < end; i++) {
        if (!pb_encode_tag_for_field(stream, field))
            return false;
        
        if (!pb_encode_varint(stream, ctx->data[i]))
            return false;
    }
    
    return true;
}

int main() {
    // Buffer pro kódovanou zprávu
    uint8_t buffer[256];
    size_t message_length;
    
    // Vytvoření pole s 10 prvky
    uint32_t values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // --- KÓDOVÁNÍ ---
    example_CustomArrayMessage message = example_CustomArrayMessage_init_zero;
    
    // Nastavení kontextu: kódovat prvky 3-7 (offset=2, count=5)
    array_context_t ctx = {
        .data = values,
        .size = 10,
        .offset = 2,
        .count = 5
    };
    
    // Nastavení callbacku
    message.values.funcs.encode = uint32_array_encode_callback;
    message.values.arg = &ctx;
    
    // Vytvoření streamu pro kódování
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    // Kódování zprávy
    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);
    
    // Výpis hex dumpu
    printf("Encoded data: ");
    for (size_t i = 0; i < message_length; i++) {
        printf("%02x ", buffer[i]);
    }
    printf("\n");
    
    // Výpis toho, co bylo zakódováno
    printf("Encoded values: ");
    for (size_t i = 2; i < 2 + 5 && i < 10; i++) {
        printf("%u ", values[i]);
    }
    printf("\n");
    
    return 0;
}

Příkaz pro kompilaci

Zkompilujte příklad s nanopb. NanoPB se obvykle používá tak, že zdrojové soubory se přímo vloží do vašeho projektu:

compile_custom_array_example.sh
gcc -o custom_array_example custom_array_example.c custom_array.pb.c pb_common.c pb_encode.c pb_decode.c -I.

Poznámka: Zdrojové soubory NanoPB (pb_common.c, pb_encode.c, pb_decode.c) je třeba kompilovat přímo s vaším projektem. Můžete je získat z NanoPB GitHub repozitáře.

Klíčové body

  • Kontextová struktura: Použijte strukturu pro předání offsetu, počtu a ukazatele na data
  • Logika offsetu: Začněte kódování na zadaném offsetu
  • Omezení počtu: Zakódujte maximálně count prvků po offsetu
  • Vzor callbacku: Použijte pb_callback_t s vlastní funkcí kódování
  • Flexibilita: Lze zakódovat libovolný řez pole bez kopírování
  • Efektivita paměti: Není potřeba vytvářet dočasná pole
  • Vzor KKS-Firmware: Založeno na ArrayConverterWithOffsetAndSize z KKS-Firmware

Kdy použít vlastní konvertory polí

  • Pokud potřebujete zakódovat podmnožinu velkého pole
  • Pokud chcete předejít kopírování dat
  • Při implementaci stránkování nebo dělení na bloky
  • Pokud rozložení pole neodpovídá požadavkům protobuf
  • Pokud potřebujete vlastní logiku kódování pro pole

Očekávaný výstup

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 

Všimněte si, že jsou zakódovány pouze prvky 3, 4, 5, 6, 7 (5 prvků začínajících od offsetu 2).

Pokročilé: Generická implementace ve stylu šablon

Pro obecnější implementaci v C (simulace šablon):

custom_array_generic.c
#include <stddef.h>
#include <limits.h>

#define MAX_OFFSET_SIZE (SIZE_MAX / 2)

typedef struct {
    const void* data;
    size_t element_size;
    size_t size;
    size_t offset;
    size_t count;
    bool (*encode_element)(pb_ostream_t*, const pb_field_t*, const void*);
} generic_array_context_t;

bool generic_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
    const generic_array_context_t* ctx = (const generic_array_context_t*)*arg;
    
    size_t start = ctx->offset;
    size_t end = (ctx->offset + ctx->count < ctx->size) ? 
                 ctx->offset + ctx->count : ctx->size;
    
    const uint8_t* data = (const uint8_t*)ctx->data;
    
    for (size_t i = start; i < end; i++) {
        const void* element = data + (i * ctx->element_size);
        if (!ctx->encode_element(stream, field, element)) {
            return false;
        }
    }
    
    return true;
}

Reálné použití

Tento vzor se používá v KKS-Firmware pro:

  • Kódování pouze aktivních kanálů z většího pole kanálů
  • Odesílání částečných dat ze senzorů pro úsporu šířky pásma
  • Implementaci diferenciálních aktualizací
  • Efektivní zpracování řídkých polí

Rozdíly oproti C++

Verze v C se od C++ liší v několika ohledech:

  • Žádné šablony: Místo toho použijte struktury a ukazatele na funkce
  • Ruční správa kontextu: Kontext musí být předán explicitně
  • Žádné std::array: Použijte obyčejná C pole se sledováním velikosti
  • Žádné std::algorithm: Iteraci implementujte ručně
  • Stejný vzor callbacku: Oba používají pb_callback_t

Další příspěvky o NanoPB


Podívejte se na podobné články podle kategorie: Embedded C/C++ Protocol Buffers