Quickstart & guides#

pw_bytes: Utilities for manipulating binary data

Quickstart#

Add @pigweed//pw_bytes to the deps list in your Bazel target:

cc_library("...") {
  # ...
  deps = [
    # ...
    "@pigweed//pw_bytes",
    # ...
  ]
}

If only one part of the module is needed, depend only on it; for example @pigweed//pw_bytes:array.

Add $dir_pw_bytes to the deps list in your pw_executable() build target:

pw_executable("...") {
  # ...
  deps = [
    # ...
    "$dir_pw_bytes",
    # ...
  ]
}

Add pw_bytes to your pw_add_library or similar CMake target:

pw_add_library(my_library STATIC
  HEADERS
    ...
  PRIVATE_DEPS
    # ...
    pw_bytes
    # ...
)

For a narrower dependency, depend on subtargets like pw_bytes.array, etc.

There are two ways to use pw_bytes from a Zephyr project:

  1. Depend on pw_bytes in your CMake target (see CMake tab). This is Pigweed Team’s suggested approach since it enables precise CMake dependency analysis.

  2. Add CONFIG_PIGWEED_BYTES=y to the Zephyr project’s configuration, which causes pw_bytes to become a global dependency and have the includes exposed to all targets. Pigweed team does not recommend this approach, though it is the typical Zephyr solution.

Byte units#

Define byte sizes clearly using IEC standard units.

Using function calls:

#include "pw_bytes/units.h"

constexpr size_t kSmallBuffer = pw::bytes::KiB(2);    // 2048 bytes
constexpr size_t kLargeBuffer = pw::bytes::MiB(1);    // 1048576 bytes
constexpr size_t kTotalMemory = pw::bytes::GiB(4);    // 4 gibibytes

Using user-defined literals (requires using namespace):

#include "pw_bytes/units.h"

using namespace pw::bytes::unit_literals;

constexpr size_t kNetworkPacketSize = 1500_B;
constexpr size_t kFlashSectorSize = 4_KiB;
constexpr size_t kRamDiskSize = 16_MiB;

Compile-time byte arrays#

Construct std::array<std::byte, N> at compile time.

From individual byte values or integers (little-endian for multi-byte integers):

#include "pw_bytes/array.h"

// Creates std::array<std::byte, 4>
constexpr auto kMagicWord = pw::bytes::Array<'M', 'A', 'G', 'K'>;

// Creates std::array<std::byte, 2> {0x34, 0x12}
constexpr auto kValue16 = pw::bytes::Array<std::byte>(0x1234);

// Creates std::array<uint8_t, 4> {0x78, 0x56, 0x34, 0x12}
constexpr auto kValue32_u8 = pw::bytes::Array<uint8_t>(0x12345678);

From string literals (without null terminator):

#include "pw_bytes/array.h"

// Creates std::array<std::byte, 5> {'H', 'e', 'l', 'l', 'o'}
constexpr auto kGreeting = pw::bytes::String("Hello");

// Creates std::array<std::byte, 0>
constexpr auto kEmpty = pw::bytes::String("");

Concatenate multiple sources:

#include "pw_bytes/array.h"
#include "pw_bytes/endian.h" // For CopyInOrder

constexpr auto kPart1 = pw::bytes::String("ID:");
constexpr uint16_t kDeviceId = 0xABCD;

// Creates std::array<std::byte, 5> {'I', 'D', ':', 0xCD, 0xAB} (kDeviceId as little-endian)
constexpr auto kFullId_LE = pw::bytes::Concat(kPart1, kDeviceId);

// Creates std::array<std::byte, 5> {'I', 'D', ':', 0xAB, 0xCD} (kDeviceId as big-endian)
constexpr auto kFullId_BE = pw::bytes::Concat(kPart1, pw::bytes::CopyInOrder(pw::endian::big, kDeviceId));

Dynamically constructed byte sequences in fixed-size buffers#

#include "pw_bytes/byte_builder.h"
#include "pw_bytes/span.h" // For pw::ByteSpan
#include "pw_log/log.h"    // For PW_LOG_ERROR (example)
#include <cstring>         // For strlen (example)

// Example functions
void ProcessPacket(pw::ConstByteSpan packet_data) { /* ... */ }
void SendData(const std::byte* data, size_t size) { /* ... */ }


void BuildPacket(pw::ByteSpan buffer) {
  pw::ByteBuilder builder(buffer);

  builder.PutUint8(0x01); // Packet type
  builder.PutUint16(0x1234, pw::endian::big); // Length in big-endian

  const char* payload_str = "DATA";
  builder.append(payload_str, strlen(payload_str));

  if (!builder.ok()) {
    // Handle error, e.g., buffer too small
    PW_LOG_ERROR("Failed to build packet: %s", builder.status().str());
    return;
  }

  // Use builder.data() and builder.size()
  ProcessPacket(pw::ConstByteSpan(builder.data(), builder.size()));
}

void ExampleWithByteBuffer() {
  pw::ByteBuffer<64> local_buffer; // Buffer is part of the ByteBuffer object

  local_buffer.PutUint32(0xAABBCCDD, pw::endian::little);
  // ... build more data ...

  if (local_buffer.ok()) {
    SendData(local_buffer.data(), local_buffer.size());
  }
}

Endianness conversion#

Convert integers to and from specific byte orders.

#include "pw_bytes/endian.h"
#include <array>
#include <cstdint>

// Convert a value to a specific byte order for storage/transmission
uint32_t native_value = 0x12345678;
uint32_t big_endian_value = pw::bytes::ConvertOrderTo(pw::endian::big, native_value);
// On a little-endian system, big_endian_value is now 0x78563412
// On a big-endian system, big_endian_value is still 0x12345678

// Copy value into a buffer with a specific order
std::array<std::byte, 4> buffer;
// pw::bytes::CopyInOrder returns a std::array<std::byte, sizeof(T)>
auto ordered_bytes = pw::bytes::CopyInOrder(pw::endian::little, native_value);
std::memcpy(buffer.data(), ordered_bytes.data(), ordered_bytes.size());
// buffer now contains {0x78, 0x56, 0x34, 0x12} on a little-endian system if native_value was 0x12345678

// Read a value from a buffer with a specific order
uint32_t read_value = pw::bytes::ReadInOrder<uint32_t>(pw::endian::little, buffer.data());
// read_value is 0x12345678 (assuming buffer was filled as above)

Alignment utilities#

Ensure data is correctly aligned in memory.

#include "pw_bytes/alignment.h"
#include <cstdint> // For std::byte, uintptr_t
#include <cstdlib> // For malloc, free (example)

// Example functions
void* GetMemory() { return std::malloc(100); }
size_t GetMemorySize() { return 100; }
void FreeMemory(void* ptr) { std::free(ptr); }

void AlignmentExample() {
  void* some_memory_block = GetMemory();
  if (!some_memory_block) return;
  size_t memory_size = GetMemorySize();

  // Align pointer up to the next uint32_t boundary
  uint32_t* aligned_ptr_u32 = pw::AlignUp(static_cast<uint32_t*>(some_memory_block), alignof(uint32_t));

  // Check if a pointer is aligned
  if (pw::IsAlignedAs<double>(aligned_ptr_u32)) {
    // This block might not be reached if alignof(uint32_t) < alignof(double)
    // and aligned_ptr_u32 didn't happen to align for double.
  }

  // Get the largest aligned subspan
  pw::ByteSpan original_span(static_cast<std::byte*>(some_memory_block), memory_size);
  pw::ByteSpan aligned_subspan = pw::GetAlignedSubspan(original_span, alignof(uint64_t));
  // aligned_subspan can now be safely used to store uint64_t values,
  // provided aligned_subspan.size() is sufficient.

  FreeMemory(some_memory_block);
}

Packed pointers#

Store extra data in the unused bits of aligned pointers.

#include "pw_bytes/packed_ptr.h"
#include <cstdint>

struct alignas(4) MyNode { // Aligned to 4 bytes, so 2 LSBs are available
  int data;
  // MyNode* next;
  // bool flag1;
  // bool flag2;
  // Instead of separate flags, pack them into the pointer:
  pw::PackedPtr<MyNode> next_and_flags;
};

MyNode node1, node2;

MyNode list_head;
list_head.next_and_flags.set(&node1);
// Store two flags (e.g., flag1=1, flag2=0). Max value for 2 bits is 0b11 (3).
list_head.next_and_flags.set_packed_value(0b01);

MyNode* next_node = list_head.next_and_flags.get();
uintptr_t flags = list_head.next_and_flags.packed_value();
// bool flag1_val = flags & 0b01;
// bool flag2_val = (flags & 0b10) >> 1;

Bit manipulation#

Perform low-level bit operations.

#include "pw_bytes/bit.h"
#include <cstdint>

// Sign extend a 12-bit signed value to int16_t
uint16_t raw_adc_value = 0x0F00; // Represents -256 in 12-bit two's complement (0b111100000000)
int16_t signed_adc_value = pw::bytes::SignExtend<12>(raw_adc_value);
// signed_adc_value is now 0xFF00 (which is -256 in int16_t)

// Extract a bitfield
uint32_t register_value = 0xAABBCCDD;
// Extract bits 15 down to 8 (inclusive)
uint8_t byte_value = pw::bytes::ExtractBits<uint8_t, 15, 8>(register_value);
// byte_value is 0xCC

Syntactic sugar for std::byte literals#

Conveniently create std::byte literals.

#include "pw_bytes/suffix.h"
#include <cstddef> // For std::byte

// Required to bring the operator into scope
using ::pw::operator""_b;

constexpr std::byte kMyByteValue = 128_b;
constexpr std::byte kAnotherByte = 0xFF_b;

void ProcessByte(std::byte b);

// ProcessByte(42_b); // Example call