NanoPB : Comment gérer les champs répétés (tableaux) en C
Voir aussi : Version C++ : Comment gérer les champs répétés/tableaux en C++
NanoPB est une implémentation de Protocol Buffers optimisée pour la taille du code, destinée aux systèmes embarqués. Ce post montre comment gérer les champs répétés (tableaux) en C avec NanoPB.
Définition Proto
Créez d’abord un fichier .proto avec des champs répétés :
syntax = "proto3";
package example;
message RepeatedMessage {
repeated uint32 values = 1;
repeated float temperatures = 2;
}Génération du code NanoPB
Générez le code NanoPB avec un fichier .options pour spécifier les tailles des tableaux :
Créez repeated.options :
example.RepeatedMessage.values max_count:20
example.RepeatedMessage.temperatures max_count:10Générez ensuite :
protoc --nanopb_out=. repeated.protoCela générera repeated.pb.h et repeated.pb.c.
Exemple C avec tableaux de taille fixe
Voici un exemple C complet utilisant des tableaux de taille fixe :
#include <stdio.h>
#include <stdint.h>
#include "repeated.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
int main() {
// Tampon pour le message encodé
uint8_t buffer[256];
size_t message_length;
// --- ENCODAGE ---
example_RepeatedMessage message = example_RepeatedMessage_init_zero;
// Définir les valeurs des champs répétés
message.values[0] = 10;
message.values[1] = 20;
message.values[2] = 30;
message.values_count = 3; // Important : définir le compte
message.temperatures[0] = 20.5f;
message.temperatures[1] = 21.0f;
message.temperatures[2] = 21.5f;
message.temperatures_count = 3; // Important : définir le compte
// Créer le flux pour l'encodage
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
// Encoder le message
if (!pb_encode(&ostream, example_RepeatedMessage_fields, &message)) {
printf("Échec de l'encodage : %s\n", PB_GET_ERROR(&ostream));
return 1;
}
message_length = ostream.bytes_written;
printf("Encodé %zu octets\n", message_length);
// Afficher le dump hexadécimal des données encodées
printf("Données encodées : ");
for (size_t i = 0; i < message_length; i++) {
printf("%02x ", buffer[i]);
}
printf("\n");
// --- DÉCODAGE ---
example_RepeatedMessage decoded = example_RepeatedMessage_init_zero;
// Créer le flux pour le décodage
pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
// Décoder le message
if (!pb_decode(&istream, example_RepeatedMessage_fields, &decoded)) {
printf("Échec du décodage : %s\n", PB_GET_ERROR(&istream));
return 1;
}
// Afficher les valeurs décodées
printf("Valeurs décodées :\n");
printf(" values (%zu éléments) : ", decoded.values_count);
for (size_t i = 0; i < decoded.values_count; i++) {
printf("%u ", (unsigned int)decoded.values[i]);
}
printf("\n");
printf(" temperatures (%zu éléments) : ", decoded.temperatures_count);
for (size_t i = 0; i < decoded.temperatures_count; i++) {
printf("%.1f ", decoded.temperatures[i]);
}
printf("\n");
return 0;
}Commande de compilation
Compilez l’exemple avec nanopb. NanoPB est généralement utilisé en incluant directement les fichiers source dans votre projet :
gcc -o repeated_example repeated_example.c repeated.pb.c pb_common.c pb_encode.c pb_decode.c -I.Remarque : Les fichiers source NanoPB (pb_common.c, pb_encode.c, pb_decode.c) doivent être compilés directement avec votre projet. Vous pouvez les obtenir depuis le dépôt GitHub NanoPB.
Script de test Python
Pour vérifier l’encodage, vous pouvez utiliser la bibliothèque protobuf de Python :
import repeated_pb2
# Lire les données binaires
with open('encoded.bin', 'rb') as f:
data = f.read()
# Décoder
msg = repeated_pb2.RepeatedMessage()
msg.ParseFromString(data)
print("Valeurs décodées par Python :")
print(f" values : {list(msg.values)}")
print(f" temperatures : {list(msg.temperatures)}")Compilez d’abord les définitions protobuf Python :
protoc --python_out=. repeated.protoModifiez ensuite l’exemple C pour enregistrer les données encodées dans un fichier :
// Après l'encodage, ajoutez ceci :
FILE *f = fopen("encoded.bin", "wb");
fwrite(buffer, 1, message_length, f);
fclose(f);Alternative : Champs répétés basés sur des callbacks
Pour une gestion dynamique des tableaux, vous pouvez utiliser des callbacks. Créez repeated_callback.options :
# Utiliser un callback pour les tableaux dynamiques
msg.RepeatedMessage.values callback
msg.RepeatedMessage.temperatures callbackRégénérez ensuite et utilisez cette approche :
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "repeated.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
typedef struct {
uint32_t* data;
size_t size;
size_t capacity;
} dynamic_uint32_array_t;
typedef struct {
float* data;
size_t size;
size_t capacity;
} dynamic_float_array_t;
// Callback d'encodage pour le tableau uint32
bool uint32_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
const dynamic_uint32_array_t* arr = (const dynamic_uint32_array_t*)*arg;
for (size_t i = 0; i < arr->size; i++) {
if (!pb_encode_tag_for_field(stream, field))
return false;
if (!pb_encode_varint(stream, arr->data[i]))
return false;
}
return true;
}
// Callback de décodage pour le tableau uint32
bool uint32_array_decode_callback(pb_istream_t *stream, const pb_field_t *field, void **arg) {
dynamic_uint32_array_t* arr = (dynamic_uint32_array_t*)*arg;
uint64_t value;
if (!pb_decode_varint(stream, &value))
return false;
// Redimensionner si nécessaire
if (arr->size >= arr->capacity) {
size_t new_capacity = arr->capacity == 0 ? 4 : arr->capacity * 2;
uint32_t* new_data = (uint32_t*)realloc(arr->data, new_capacity * sizeof(uint32_t));
if (!new_data)
return false;
arr->data = new_data;
arr->capacity = new_capacity;
}
arr->data[arr->size++] = (uint32_t)value;
return true;
}
// Callback d'encodage pour le tableau float
bool float_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
const dynamic_float_array_t* arr = (const dynamic_float_array_t*)*arg;
for (size_t i = 0; i < arr->size; i++) {
if (!pb_encode_tag_for_field(stream, field))
return false;
// Encoder le float comme 4 octets
union { float f; uint32_t u; } u;
u.f = arr->data[i];
if (!pb_encode_varint(stream, u.u))
return false;
}
return true;
}
// Callback de décodage pour le tableau float
bool float_array_decode_callback(pb_istream_t *stream, const pb_field_t *field, void **arg) {
dynamic_float_array_t* arr = (dynamic_float_array_t*)*arg;
uint64_t value;
if (!pb_decode_varint(stream, &value))
return false;
// Redimensionner si nécessaire
if (arr->size >= arr->capacity) {
size_t new_capacity = arr->capacity == 0 ? 4 : arr->capacity * 2;
float* new_data = (float*)realloc(arr->data, new_capacity * sizeof(float));
if (!new_data)
return false;
arr->data = new_data;
arr->capacity = new_capacity;
}
union { float f; uint32_t u; } u;
u.u = (uint32_t)value;
arr->data[arr->size++] = u.f;
return true;
}
int main() {
uint8_t buffer[256];
size_t message_length;
uint32_t values_data[] = {10, 20, 30};
float temperatures_data[] = {20.5f, 21.0f, 21.5f};
dynamic_uint32_array_t values = {values_data, 3, 3};
dynamic_float_array_t temperatures = {temperatures_data, 3, 3};
// --- ENCODAGE ---
example_RepeatedMessage message = example_RepeatedMessage_init_zero;
message.values.funcs.encode = uint32_array_encode_callback;
message.values.arg = &values;
message.temperatures.funcs.encode = float_array_encode_callback;
message.temperatures.arg = &temperatures;
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&ostream, example_RepeatedMessage_fields, &message)) {
printf("Échec de l'encodage : %s\n", PB_GET_ERROR(&ostream));
return 1;
}
message_length = ostream.bytes_written;
printf("Encodé %zu octets\n", message_length);
// --- DÉCODAGE ---
example_RepeatedMessage decoded = example_RepeatedMessage_init_zero;
dynamic_uint32_array_t decoded_values = {NULL, 0, 0};
dynamic_float_array_t decoded_temperatures = {NULL, 0, 0};
decoded.values.funcs.decode = uint32_array_decode_callback;
decoded.values.arg = &decoded_values;
decoded.temperatures.funcs.decode = float_array_decode_callback;
decoded.temperatures.arg = &decoded_temperatures;
pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
if (!pb_decode(&istream, example_RepeatedMessage_fields, &decoded)) {
printf("Échec du décodage : %s\n", PB_GET_ERROR(&istream));
return 1;
}
printf("Valeurs décodées :\n");
printf(" values (%zu éléments) : ", decoded_values.size);
for (size_t i = 0; i < decoded_values.size; i++) {
printf("%u ", decoded_values.data[i]);
}
printf("\n");
printf(" temperatures (%zu éléments) : ", decoded_temperatures.size);
for (size_t i = 0; i < decoded_temperatures.size; i++) {
printf("%.1f ", decoded_temperatures.data[i]);
}
printf("\n");
// Libérer la mémoire allouée
free(decoded_values.data);
free(decoded_temperatures.data);
return 0;
}Points clés
- Tableaux de taille fixe : Utilisez
max_countdans le fichier .options pour une allocation statique simple - Basé sur des callbacks : Utilisez
callbackdans le fichier .options pour une gestion dynamique des tableaux - Taille fixe : Définissez le champ
*_countpour spécifier le nombre d’éléments - Basé sur des callbacks : Implémentez des callbacks d’encodage/décodage avec gestion manuelle de la mémoire
- Les tableaux sont encodés comme des tags de champs répétés avec des valeurs
- Utilisez
malloc/realloc/freeen C pour la gestion dynamique des tableaux - Vérifiez toujours les comptes de tableau pour éviter les dépassements
- N’oubliez pas de libérer la mémoire allouée dans l’approche basée sur des callbacks
Quand utiliser quelle approche
- Tableaux de taille fixe : Quand vous connaissez la taille maximale du tableau et que vous voulez un code simple
- Basé sur des callbacks : Quand la taille du tableau est variable ou que vous avez besoin d’une allocation dynamique de mémoire
Sortie attendue
Encoded 15 bytes
Encoded data: 08 0a 08 14 08 1e 15 00 00 a4 41 15 00 00 a8 41 15 00 00 ac 41
Decoded values:
values (3 items): 10 20 30
temperatures (3 items): 20.5 21.0 21.5 Différences par rapport au C++
La version C est presque identique à la version C++, avec des différences clés :
- Utilisez
malloc/realloc/freeau lieu destd::vectorpour les tableaux dynamiques - Gestion manuelle de la mémoire requise (pas de destructeurs)
- Utilisez une structure pour l’enveloppe de tableau dynamique au lieu de std::vector
- N’oubliez pas de libérer la mémoire allouée après utilisation
- Pas de gestion automatique de la mémoire en C
Plus de posts sur NanoPB
- Types scalaires de base en C++
- Types scalaires de base en C
- Types chaîne en C++
- Types chaîne en C
- Types bytes en C++
- Types bytes en C
- Champs optionnels en C++
- Champs optionnels en C
- Champs répétés/tableaux en C++
- Enums en C++
- Enums en C
- Messages imbriqués en C++
- Messages imbriqués en C
- Types oneof/union en C++
- Types oneof/union en C
- Convertisseurs de tableaux personnalisés en C++
- Convertisseurs de tableaux personnalisés en C