Skip to content

C Language Support

Status: ✅ Full Support
Examples: examples/c-protobuf-c/, examples/c-nanopb/

C support provides multiple Protocol Buffer implementations optimized for different use cases, from standard systems to embedded microcontrollers.

ImplementationDescriptionTarget Use CaseGenerated Files
protobuf-cStandard C implementationGeneral purpose C applications*.pb-c.h, *.pb-c.c
nanopbLightweight implementationEmbedded systems, microcontrollers*.pb.h, *.pb.c
upbGoogle’s C implementationHigh-performance parsing (future)*.upb.h, *.upb.c
{
  inputs = {
    bufrnix.url = "github:conneroisu/bufr.nix";
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
  };

  outputs = inputs @ {
    self,
    nixpkgs,
    flake-parts,
    ...
  }:
    flake-parts.lib.mkFlake {inherit self;} {
      imports = [
        inputs.bufrnix.flakeModule
      ];
      systems = ["x86_64-linux" "aarch64-linux"];
      perSystem = {
        config,
        self',
        inputs',
        pkgs,
        system,
        ...
      }: {
        bufrnix.languages.c = {
          enable = true;
          outputPath = "gen/c";

          # Choose your implementation
          protobuf-c.enable = true; # For general C applications
          # OR
          nanopb.enable = true; # For embedded systems
        };
      };
    };
}
languages.c = {
enable = true;
outputPath = "gen/c";
# Standard C implementation
protobuf-c = {
enable = true;
outputPath = "gen/c/protobuf-c";
options = []; # protoc-gen-c options
};
# Embedded systems implementation
nanopb = {
enable = true;
outputPath = "gen/c/nanopb";
options = [];
# Nanopb-specific configuration
maxSize = 256; # Max size for dynamic allocation
fixedLength = true; # Use fixed-length arrays
noUnions = false; # Allow union (oneof) support
msgidType = ""; # Message ID type for RPC
};
# Google's upb (future support)
upb = {
enable = false; # Not yet available in nixpkgs
outputPath = "gen/c/upb";
options = [];
};
};
proto/example/v1/example.proto
syntax = "proto3";
package example.v1;
message Example {
string id = 1;
string name = 2;
int32 value = 3;
repeated string tags = 4;
ExampleType type = 5;
bytes data = 6;
}
enum ExampleType {
EXAMPLE_TYPE_UNSPECIFIED = 0;
EXAMPLE_TYPE_BASIC = 1;
EXAMPLE_TYPE_ADVANCED = 2;
}
message NestedExample {
Example example = 1;
map<string, string> metadata = 2;
repeated Example examples = 3;
}
proto/sensor/v1/sensor.proto
syntax = "proto3";
package sensor.v1;
// Optimized for embedded systems
message SensorReading {
uint32 timestamp = 1;
float temperature = 2;
float humidity = 3;
uint32 pressure = 4;
repeated float values = 5; // Fixed array in nanopb
SensorStatus status = 6;
}
enum SensorStatus {
SENSOR_STATUS_UNKNOWN = 0;
SENSOR_STATUS_OK = 1;
SENSOR_STATUS_ERROR = 2;
SENSOR_STATUS_MAINTENANCE = 3;
}
message DeviceConfig {
string device_id = 1; // Max 32 chars in nanopb
uint32 sample_rate = 2;
bool enable_logging = 3;
repeated uint32 calibration = 4; // Fixed size array
}
# sensor.options - Constraints for embedded systems
sensor.v1.DeviceConfig.device_id max_size:32
sensor.v1.SensorReading.values max_count:10 fixed_count:true
sensor.v1.DeviceConfig.calibration max_count:8 fixed_count:true
* type:FT_STATIC # Use static allocation
#include "proto/gen/c/protobuf-c/example/v1/example.pb-c.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// Create and initialize a message
Example__V1__Example example = EXAMPLE__V1__EXAMPLE__INIT;
example.id = "test-123";
example.name = "Test Example";
example.value = 42;
example.type = EXAMPLE__V1__EXAMPLE_TYPE__BASIC;
// Add tags (dynamic allocation)
char *tags[] = {"important", "test", "production"};
example.tags = malloc(3 * sizeof(char*));
for (int i = 0; i < 3; i++) {
example.tags[i] = strdup(tags[i]);
}
example.n_tags = 3;
// Add binary data
uint8_t binary_data[] = {0x01, 0x02, 0x03, 0x04};
example.data.data = binary_data;
example.data.len = sizeof(binary_data);
// Serialize the message
size_t size = example__v1__example__get_packed_size(&example);
uint8_t *buffer = malloc(size);
size_t packed_size = example__v1__example__pack(&example, buffer);
printf("Serialized %zu bytes (expected %zu)\n", packed_size, size);
// Deserialize the message
Example__V1__Example *unpacked =
example__v1__example__unpack(NULL, packed_size, buffer);
if (unpacked) {
printf("Unpacked message:\n");
printf(" ID: %s\n", unpacked->id);
printf(" Name: %s\n", unpacked->name);
printf(" Value: %d\n", unpacked->value);
printf(" Type: %d\n", unpacked->type);
printf(" Tags (%zu):\n", unpacked->n_tags);
for (size_t i = 0; i < unpacked->n_tags; i++) {
printf(" - %s\n", unpacked->tags[i]);
}
// Free the unpacked message
example__v1__example__free_unpacked(unpacked, NULL);
}
// Cleanup
for (int i = 0; i < 3; i++) {
free(example.tags[i]);
}
free(example.tags);
free(buffer);
return 0;
}
Featureprotobuf-cnanopbupb (future)
Dynamic Allocation
Fixed Arrays
Code SizeMediumSmallSmall
Memory UsageDynamicStaticArena
Embedded Systems⚠️ Limited✅ Excellent⚠️ Limited
Full Proto3⚠️ Subset
Services/RPCBasic
Extensions
Options Files
Zero-Copy
JSON Support
  • Building standard C applications
  • Need full Protocol Buffers compatibility
  • Dynamic memory allocation is acceptable
  • Integrating with existing C projects
  • Working with complex message structures
  • Need map support and all Proto3 features
  • Targeting microcontrollers (Arduino, STM32, ESP32, etc.)
  • Memory is severely constrained (ie less than 100KB RAM)
  • Need predictable memory usage
  • Building real-time systems
  • Want zero dynamic allocation
  • Code size matters more than features
  • Working with simple message structures
  • Need high-performance parsing
  • Want zero-copy operations
  • Building high-throughput systems
  • Need both performance and full features
// Dynamic allocation for repeated fields
message->n_items = 3;
message->items = malloc(3 * sizeof(Item*));
for (int i = 0; i < 3; i++) {
message->items[i] = malloc(sizeof(Item));
}
// Remember to free everything
for (int i = 0; i < message->n_items; i++) {
free(message->items[i]);
}
free(message->items);
// Fixed allocation at compile time
MyMessage msg = MyMessage_init_zero;
msg.items_count = 3; // Max defined in .options file
msg.items[0] = 1;
msg.items[1] = 2;
msg.items[2] = 3;
// No free() needed - stack allocated
Terminal window
cd examples/c-protobuf-c
nix build
nix develop
make
./example
Terminal window
cd examples/c-nanopb
nix build
nix develop
make
./sensor_example
  1. Choose the Right Implementation: protobuf-c for general use, nanopb for embedded
  2. Memory Management: Always free protobuf-c allocations, nanopb is stack-based
  3. Error Handling: Check return values from encode/decode functions
  4. Buffer Sizing: Size buffers appropriately for your message sizes
  5. Options Files: Use .options files with nanopb to control memory usage
  6. Testing: Test with real data sizes and memory constraints

Memory Leaks: Always use *_free_unpacked() functions:

Message *msg = message__unpack(NULL, len, data);
// Use message...
message__free_unpacked(msg, NULL);

Segmentation Faults: Initialize messages properly:

Message msg = MESSAGE__INIT; // Always initialize

Buffer Overflow: Ensure buffers are sized correctly:

uint8_t buffer[256]; // Size based on your message
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));

Field Size Errors: Update .options files for string/array limits:

MyMessage.my_string max_size:64
MyMessage.my_array max_count:10