Phase 4 · Advanced C++ & Systems ProgrammingModule 25~60 min read

Binary Data, Interoperability & Low-Level C++

Work safely at platform boundaries with bytes, alignment, bit operations, C APIs, operating-system resources, and explicit wrappers.

What you'll learn

Low-level work is safe only when representation, alignment, aliasing, ownership, and platform contracts are explicit. You will inspect bytes without violating the object model, encode data portably, call C APIs, and wrap native resources behind RAII boundaries.

By the end, you'll be able to:

  • Explain object representation, padding, alignment, and byte access
  • Use bit utilities and explicit endianness for binary protocols
  • Distinguish trivial and standard-layout properties without overgeneralizing
  • Build narrow C and operating-system interop wrappers with clear ownership

Object representation and alignment

An object occupies sizeof(T) bytes at an address satisfyingalignof(T). Its object representation includes value bytes and possible padding. Padding values may be indeterminate, so bytewise equality of class objects does not imply semantic equality. std::byte represents raw storage without pretending it is a character or number.

representation.cpp
#include <cstddef>
#include <iostream>
#include <span>

struct PacketHeader {
    std::uint16_t kind;
    std::uint32_t length;
};

int main() {
    PacketHeader header{7, 1024};
    auto bytes{std::as_bytes(std::span{&header, 1})};
    std::cout << "size: " << sizeof(header) << '\n';
    std::cout << "alignment: " << alignof(PacketHeader) << '\n';
    std::cout << "representation bytes: " << bytes.size() << '\n';
}
Exact size, alignment, padding, and byte values are implementation-dependent.

Watch out

A struct's in-memory representation is not automatically a wire or file format. Padding and native byte order can differ across builds and machines.

Bits and endianness

Unsigned integers are the clearest base for bit masks and shifts. The <bit>header provides rotations, population counts, powers-of-two helpers, bit_cast, and native endianness detection. A protocol must select a byte order and encode each field explicitly rather than copying native integers into a buffer.

encode_u32.cpp
#include <array>
#include <cstdint>
#include <iomanip>
#include <iostream>

std::array<std::uint8_t, 4> encode_big_endian(std::uint32_t value) {
    return {
        static_cast<std::uint8_t>(value >> 24),
        static_cast<std::uint8_t>(value >> 16),
        static_cast<std::uint8_t>(value >> 8),
        static_cast<std::uint8_t>(value)
    };
}

int main() {
    for (auto byte : encode_big_endian(0x12345678u)) {
        std::cout << std::hex << static_cast<unsigned>(byte) << ' ';
    }
}

Key idea

Decode into a sufficiently wide unsigned type, validate lengths before reading, and shift unsigned values. Signed shifts and unchecked offsets invite undefined behavior and security defects.

Trivial and standard-layout types

Type traits describe specific language properties. Trivially copyable types may have their representations copied with memcpy and restored under defined conditions. Standard-layout types have layout guarantees useful for some C interop. Neither property alone makes raw serialization portable or makes every byte pattern a valid value.

PropertyUseful guaranteeDoes not guarantee
Trivially copyableRepresentation can be copied as bytesStable cross-platform format
Standard layoutRestricted, C-like member layout rulesNo padding
Unique object representationsNo distinct representations for equal valuesSemantic protocol validity
Implicit lifetimeCertain operations may begin object lifetime in storageArbitrary type punning

Aliasing and safe representation access

Accessing storage through an unrelated object type can violate strict-aliasing and lifetime rules. Use std::bit_cast for same-size trivially copyable value reinterpretation,memcpy for representation transfer, and character or byte views for inspection. Do not use reinterpret_cast as a universal conversion license.

bit_cast.cpp
#include <bit>
#include <cstdint>
#include <iomanip>
#include <iostream>

int main() {
    float value{1.0f};
    static_assert(sizeof(float) == sizeof(std::uint32_t));
    auto bits{std::bit_cast<std::uint32_t>(value)};
    std::cout << "0x" << std::hex << bits << '\n';
}
The displayed IEEE-754 pattern is common but float representation is implementation-defined.

Note

volatile is for specific observable accesses such as memory-mapped device registers. It is not thread synchronization and does not make compound operations atomic.

C interoperability

C APIs expose functions, pointers, buffers, error codes, callbacks, and opaque handles.extern "C" requests C language linkage for declarations; it does not convert C semantics into C++. Keep the boundary narrow, validate sizes and nullability, and translate raw errors and ownership into C++ types immediately.

library_api.h + wrapper.cpp
// C header
#ifdef __cplusplus
extern "C" {
#endif
struct library_handle;
library_handle* library_open(const char* path);
void library_close(library_handle*);
#ifdef __cplusplus
}
#endif

// C++ wrapper
#include <memory>
#include <stdexcept>

struct CloseLibrary {
    void operator()(library_handle* handle) const noexcept {
        if (handle) library_close(handle);
    }
};
using Library = std::unique_ptr<library_handle, CloseLibrary>;

Library open_library(const char* path) {
    Library result{library_open(path)};
    if (!result) throw std::runtime_error{"library_open failed"};
    return result;
}

Tip

Never let a C++ exception cross a C callback boundary. Catch inside the C++ adapter and translate to the documented C error channel.

Native resources and platform APIs

File descriptors, sockets, windows, mappings, and device handles each have distinct invalid values and release functions. Give each one a move-only RAII wrapper, keep platform headers behind source-file boundaries, and expose standard C++ values, spans, paths, durations, and error types to the rest of the application.

  • Store the precise native handle type, not an integer chosen for convenience
  • Represent invalid/empty state explicitly and make destruction idempotent
  • Delete copying and implement noexcept moves for exclusive resources
  • Validate buffer pointer-plus-length pairs before calling native code
  • Unmap memory before its backing handle or file is released
  • Centralize feature detection and platform-specific compilation

Watch out

Memory mapping does not validate file contents and does not make out-of-range access safe. The mapping, backing resource, parsed offsets, and every derived view have linked lifetimes.

Recap & quick check

Key takeaways

  • Object representation includes possible padding and is not a portable serialization format.
  • Binary protocols require explicit widths, byte order, bounds, and validation.
  • Trivially copyable and standard-layout are narrow properties, not universal safety labels.
  • bit_cast, memcpy, and byte views are safer than unrelated-type pointer dereferencing.
  • C and platform boundaries should be narrow adapters that immediately establish RAII and typed contracts.

Quick check

1. Does sizeof(struct) equal the sum of member sizes?

2. What should a binary format specify?

3. What is std::bit_cast for?

4. Does volatile synchronize threads?

Phase 4 complete. Next, Module 26 — Debugging, Testing & Undefined Behavior begins the professional engineering phase.