NanoPB : Comment gérer les types bytes en C
Voir aussi : Version C++ : Comment gérer les types bytes en C++
NanoPB est une implémentation de Protocol Buffers optimisée pour la taille du code, destinée aux systèmes embarqués. Cet article montre comment gérer les types bytes en C avec NanoPB.
Définition proto
Créez d’abord un fichier .proto avec des champs bytes :
syntax = "proto3";
package example;
message BytesMessage {
bytes data = 1;
bytes signature = 2;
}Générer le code NanoPB
Générez le code NanoPB avec un fichier .options pour spécifier les tailles de tampon bytes :
Créez bytes.options :
example.BytesMessage.data max_size:32
example.BytesMessage.signature max_size:16Puis générez :
protoc --nanopb_out=. bytes.protoCela générera bytes.pb.h et bytes.pb.c.
Exemple en C avec tampons de taille fixe
Voici un exemple complet en C utilisant des tampons de taille fixe :
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "bytes.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_BytesMessage message = example_BytesMessage_init_zero;
// Définir les données bytes
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
const uint8_t signature[] = {0xAA, 0xBB, 0xCC, 0xDD};
message.data.size = sizeof(data);
memcpy(message.data.bytes, data, sizeof(data));
message.signature.size = sizeof(signature);
memcpy(message.signature.bytes, signature, sizeof(signature));
// Créer un flux pour l'encodage
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
// Encoder le message
if (!pb_encode(&ostream, example_BytesMessage_fields, &message)) {
printf("Encoding failed: %s\n", PB_GET_ERROR(&ostream));
return 1;
}
message_length = ostream.bytes_written;
printf("Encoded %zu bytes\n", message_length);
// Afficher le dump hexadécimal des données encodées
printf("Encoded data: ");
for (size_t i = 0; i < message_length; i++) {
printf("%02x ", buffer[i]);
}
printf("\n");
// --- DÉCODAGE ---
example_BytesMessage decoded = example_BytesMessage_init_zero;
// Créer un 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_BytesMessage_fields, &decoded)) {
printf("Decoding failed: %s\n", PB_GET_ERROR(&istream));
return 1;
}
// Afficher les valeurs décodées
printf("Decoded values:\n");
printf(" data: ");
for (size_t i = 0; i < decoded.data.size; i++) {
printf("%02x ", decoded.data.bytes[i]);
}
printf("\n");
printf(" signature: ");
for (size_t i = 0; i < decoded.signature.size; i++) {
printf("%02x ", decoded.signature.bytes[i]);
}
printf("\n");
return 0;
}Commande de compilation
Compilez l’exemple avec nanopb. NanoPB est généralement utilisé en incluant directement les fichiers sources dans votre projet :
gcc -o bytes_example bytes_example.c bytes.pb.c pb_common.c pb_encode.c pb_decode.c -I.Remarque : Les fichiers sources 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 bytes_pb2
# Lire les données binaires
with open('encoded.bin', 'rb') as f:
data = f.read()
# Décoder
msg = bytes_pb2.BytesMessage()
msg.ParseFromString(data)
print("Python decoded values:")
print(f" data: {msg.data.hex()}")
print(f" signature: {msg.signature.hex()}")Compilez d’abord les définitions protobuf Python :
protoc --python_out=. bytes.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 : bytes basé sur des callbacks
Pour une gestion dynamique des bytes, vous pouvez utiliser des callbacks. Créez bytes_callback.options :
# Utiliser un callback pour les bytes dynamiques
msg.BytesMessage.data callback
msg.BytesMessage.signature callbackRégénérez ensuite et utilisez cette approche :
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "bytes.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
typedef struct {
uint8_t* data;
size_t size;
} dynamic_bytes_t;
// Callback d'encodage pour les bytes
bool bytes_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
const dynamic_bytes_t* bytes = (const dynamic_bytes_t*)*arg;
if (!pb_encode_tag_for_field(stream, field))
return false;
return pb_encode_string(stream, bytes->data, bytes->size);
}
// Callback de décodage pour les bytes
bool bytes_decode_callback(pb_istream_t *stream, const pb_field_t *field, void **arg) {
dynamic_bytes_t* bytes = (dynamic_bytes_t*)*arg;
// Allouer le tampon
size_t size = stream->bytes_left;
bytes->data = (uint8_t*)malloc(size);
if (!bytes->data) {
return false;
}
bytes->size = size;
// Lire les données dans le tampon
if (!pb_read(stream, bytes->data, size)) {
free(bytes->data);
bytes->data = NULL;
bytes->size = 0;
return false;
}
return true;
}
int main() {
uint8_t buffer[256];
size_t message_length;
uint8_t data_array[] = {0x01, 0x02, 0x03, 0x04, 0x05};
uint8_t signature_array[] = {0xAA, 0xBB, 0xCC, 0xDD};
dynamic_bytes_t data = {data_array, sizeof(data_array)};
dynamic_bytes_t signature = {signature_array, sizeof(signature_array)};
// --- ENCODAGE ---
example_BytesMessage message = example_BytesMessage_init_zero;
message.data.funcs.encode = bytes_encode_callback;
message.data.arg = &data;
message.signature.funcs.encode = bytes_encode_callback;
message.signature.arg = &signature;
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&ostream, example_BytesMessage_fields, &message)) {
printf("Encoding failed: %s\n", PB_GET_ERROR(&ostream));
return 1;
}
message_length = ostream.bytes_written;
printf("Encoded %zu bytes\n", message_length);
// --- DÉCODAGE ---
example_BytesMessage decoded = example_BytesMessage_init_zero;
dynamic_bytes_t decoded_data = {NULL, 0};
dynamic_bytes_t decoded_signature = {NULL, 0};
decoded.data.funcs.decode = bytes_decode_callback;
decoded.data.arg = &decoded_data;
decoded.signature.funcs.decode = bytes_decode_callback;
decoded.signature.arg = &decoded_signature;
pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
if (!pb_decode(&istream, example_BytesMessage_fields, &decoded)) {
printf("Decoding failed: %s\n", PB_GET_ERROR(&istream));
return 1;
}
printf("Decoded values:\n");
printf(" data: ");
for (size_t i = 0; i < decoded_data.size; i++) {
printf("%02x ", decoded_data.data[i]);
}
printf("\n");
printf(" signature: ");
for (size_t i = 0; i < decoded_signature.size; i++) {
printf("%02x ", decoded_signature.data[i]);
}
printf("\n");
// Libérer la mémoire allouée
free(decoded_data.data);
free(decoded_signature.data);
return 0;
}Points clés
- Tampons de taille fixe : Utilisez
max_sizedans le fichier .options pour une allocation statique simple - Basé sur des callbacks : Utilisez
callbackdans le fichier .options pour une gestion dynamique des bytes - Taille fixe : Définissez le champ
*_sizeet utilisezmemcpypour copier les bytes - Basé sur des callbacks : Implémentez des callbacks d’encodage/décodage avec gestion manuelle de la mémoire
- Les bytes sont similaires aux chaînes mais contiennent des données binaires brutes (pas de terminaison nulle)
- Utilisez
malloc/freeen C pour la gestion dynamique des bytes - Vérifiez toujours les tailles de tampon pour éviter les débordements
- N’oubliez pas de libérer la mémoire allouée dans l’approche basée sur des callbacks
Quand utiliser quelle approche
- Tampons de taille fixe : Lorsque vous connaissez la taille maximale des bytes et souhaitez un code simple
- Basé sur des callbacks : Lorsque la taille des bytes est variable ou que vous avez besoin d’une allocation dynamique de mémoire
Sortie attendue
Encoded 14 bytes
Encoded data: 0a 05 01 02 03 04 05 12 04 aa bb cc dd
Decoded values:
data: 01 02 03 04 05
signature: aa bb cc dd Différences par rapport au C++
La version C est presque identique à la version C++, avec des différences clés :
- Utilisez
malloc/freeau lieu destd::vectorpour les bytes dynamiques - Gestion manuelle de la mémoire requise (pas de destructeurs)
- Utilisez une struct pour le wrapper de bytes dynamiques au lieu de std::vector
- N’oubliez pas de libérer la mémoire allouée après usage
- Aucune gestion automatique de la mémoire en C
Cas d’usage des bytes
- Données binaires (images, audio, etc.)
- Signatures cryptographiques
- Données brutes de capteurs
- Protocoles binaires personnalisés
- Toute donnée qui ne doit pas être interprétée comme du texte
Plus d’articles 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++
- 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 tableau personnalisés en C++
- Convertisseurs de tableau personnalisés en C