qtty-cpp 0.4.5
Header-only C++ wrapper for qtty
Loading...
Searching...
No Matches
qtty-cpp Architecture

This document describes the architectural design of qtty-cpp, a type-safe C++17 wrapper over the Rust-based qtty-ffi library.

Table of Contents

  • Overview
  • Layered Architecture
  • Data Flow for Conversions
  • Compound Units and Velocity
  • Error Propagation
  • Code Generation Pipeline
  • Build System Integration

Overview

qtty-cpp provides a modern C++ interface to physical quantities with compile-time type safety, wrapping the high-performance Rust qtty-ffi library. The architecture consists of three main layers:

  1. Rust qtty-ffi Layer - The foundational C FFI library exposing unit conversions and quantity operations
  2. Generated C++ Headers - Automatically generated type-safe wrappers for all supported units
  3. C++ Template Wrapper - The Quantity<> template providing operator overloading and compile-time safety

Layered Architecture

Layer 1: Rust qtty-ffi Library

Location: qtty/qtty-ffi/ (git submodule)

The qtty-ffi library is a Rust crate that provides:

  • Core physical quantity types and conversions
  • C-compatible FFI functions (qtty_quantity_make, qtty_quantity_convert, etc.)
  • Unit ID enumeration exported in qtty/qtty-ffi/include/qtty_ffi.h
  • Shared library artifacts (libqtty_ffi.so/.dylib/.dll)

Key C FFI interface (defined in qtty_ffi.h):

typedef struct {
double value;
UnitId unit_id;
} qtty_quantity_t;
int32_t qtty_quantity_make(double value, UnitId unit_id, qtty_quantity_t* out);
int32_t qtty_quantity_convert(qtty_quantity_t src, UnitId target_unit, qtty_quantity_t* out);
uint32_t qtty_ffi_version(void);

Unit IDs are defined as discriminants organized by dimension:

  • Length: 10000-19999
  • Time: 20000-29999
  • Angle: 30000-39999
  • Mass: 40000-49999
  • Power: 50000-59999

Layer 2: Generated C++ Headers

Location: include/qtty/units/*.hpp, include/qtty/literals.hpp

These headers are automatically generated by the Rust gen_cpp_units tool (see Code Generation Pipeline) and provide:

Unit Tag Structs: Empty tag types for template specialization

struct MeterTag {};
struct SecondTag {};
struct RadianTag {};
// ... etc.

UnitTraits Specializations: Map each tag to its C FFI unit ID

template<> struct UnitTraits<MeterTag> {
static constexpr UnitId unit_id() { return UNIT_ID_METER; }
};

Type Aliases: Convenient names for quantity types

using Meter = Quantity<MeterTag>;
using Second = Quantity<SecondTag>;

User-Defined Literals: Intuitive quantity construction

constexpr Meter operator""_m(long double value) {
return Meter(static_cast<double>(value));
}

Generated headers are organized by dimension:

Layer 3: C++ Template Wrapper

Location: include/qtty/ffi_core.hpp

The core Quantity<UnitTag> template provides:

Type Safety: Different unit types cannot be accidentally mixed

Meter m(10.0);
Second s(5.0);
// m + s; // Compile error! Different types.

Natural Operations: Operator overloading for intuitive syntax

Meter a(10.0), b(5.0);
Meter sum = a + b; // Addition
Meter scaled = a * 2.0; // Scalar multiplication
bool greater = a > b; // Comparison

Safe Conversions: Type-checked unit conversions at runtime

Meter m(1000.0);
Kilometer km = m.to<Kilometer>(); // Type-safe conversion
Quantity< typename ExtractTag< TargetType >::type > to() const
Definition ffi_core.hpp:230

Template Design:

  • The Quantity template is parameterized by a unit tag type
  • All operations preserve type safety through template metaprogramming
  • The ExtractTag<> helper allows .to<>() to accept both tags and quantity types
  • Conversions call through to the Rust FFI layer via qtty_quantity_convert

Key members (lines 81-196 in ffi_core.hpp):

  • value() - Get raw numeric value
  • to<TargetType>() - Convert to another unit
  • Arithmetic operators: +, -, *, / (scalar)
  • Comparison operators: ==, !=, <, >, <=, >=
  • Compound assignment: +=, -=, *=, /=
  • Utility: abs(), unary -

Data Flow for Conversions

When converting between units, the data flows through all three layers:

C++ User Code
|
| km.to<Meter>()
v
Quantity<KilometerTag>::to<MeterTag>()
|
| Constructs qtty_quantity_t with km value and UNIT_ID_KILOMETER
v
qtty_quantity_convert(src_qty, UNIT_ID_METER, &dst_qty)
|
| (Rust FFI implementation performs conversion)
v
Returns converted qtty_quantity_t
|
| dst_qty.value extracted
v
Return Quantity<MeterTag>(converted_value)

Step-by-step (see lines 104-115 in ffi_core.hpp):

  1. Create source quantity: qtty_quantity_make(m_value, unit_id(), &src_qty)
  2. Call conversion: qtty_quantity_convert(src_qty, target_unit_id, &dst_qty)
  3. Check status: check_status(status, "Converting units")
  4. Wrap result: return Quantity<TargetTag>(dst_qty.value)

Error checking happens via check_status() which throws typed exceptions on failure.

Compound Units and Velocity

Problem: Some physical quantities are products or quotients of base dimensions (e.g., velocity = length/time). The C FFI only supports base dimensions.

Solution: C++ template metaprogramming creates compound units without FFI support.

Implementation

Location: include/qtty/units/velocity.hpp

CompoundTag Template (line 10-11):

template<typename NumeratorTag, typename DenominatorTag>
struct CompoundTag {};

This tag encodes two unit types into a single compound type.

Division Operator Overload (lines 22-28):

template<typename LengthTag, typename TimeTag>
Quantity<CompoundTag<LengthTag, TimeTag>> operator/(
const Quantity<LengthTag>& length,
const Quantity<TimeTag>& time)
{
return Quantity<CompoundTag<LengthTag, TimeTag>>(length.value() / time.value());
}

This operator allows:

Meter distance(100.0);
Second time(20.0);
auto velocity = distance / time; // Type: Quantity<CompoundTag<MeterTag, SecondTag>>

Type Aliases (lines 31-32):

using MeterPerSecond = Quantity<CompoundTag<MeterTag, SecondTag>>;
using KilometerPerHour = Quantity<CompoundTag<KilometerTag, HourTag>>;

Limitations:

  • Compound units are created by simple arithmetic (no dimensional analysis in FFI)
  • Cannot convert between different compound unit combinations (e.g., m/s to km/h)
  • Conversions require manual decomposition and recomposition

Error Propagation

The qtty-ffi C API uses integer status codes. qtty-cpp translates these into C++ exceptions for idiomatic error handling.

Exception Hierarchy

Location: include/qtty/ffi_core.hpp (lines 14-35)

std::runtime_error
|
+-- QttyException (base for all qtty errors)
|
+-- InvalidUnitError (unknown unit ID)
|
+-- IncompatibleDimensionsError (e.g., converting length to time)
|
+-- ConversionError (invalid numeric value)

Error Translation

The check_status() function (lines 38-56) maps C status codes to C++ exceptions:

C Status Code C++ Exception
QTTY_OK No exception
QTTY_ERR_UNKNOWN_UNIT InvalidUnitError
QTTY_ERR_INCOMPATIBLE_DIM IncompatibleDimensionsError
QTTY_ERR_NULL_OUT QttyException
QTTY_ERR_INVALID_VALUE ConversionError
Other QttyException (generic)

Usage Example:

try {
Meter m(100.0);
Second s = m.to<Second>(); // Throws IncompatibleDimensionsError
} catch (const IncompatibleDimensionsError& e) {
std::cerr << "Cannot convert: " << e.what() << '\n';
}

All operations that call through to the FFI layer invoke check_status() immediately after FFI calls.

Code Generation Pipeline

Why Generation?

The Rust qtty-ffi library supports dozens of units across multiple dimensions. Manually maintaining C++ wrappers for each unit would be error-prone and tedious. Instead, the gen_cpp_units tool automatically generates:

  • Tag structs
  • UnitTraits specializations
  • Type aliases
  • User-defined literals

Generator Design

Location: gen_cpp_units/src/main.rs

Input: qtty/qtty-ffi/units.csv Output: include/qtty/units/*.hpp and include/qtty/literals.hpp

Parsing Strategy

The generator uses regex-based parsing to extract unit definitions from comments in the C header:

Pattern (line 64):

unit_pattern = r'/\*\s*(\w+)\s*\‍(([^)]+)\‍)\s*\*/\s*UNIT_ID_(\w+)\s*=\s*(\d+)'

This matches:

/* Meter (m) */ UNIT_ID_METER = 10012,
^display_name ^symbol ^const_name ^discriminant

Dimension Mapping (lines 70-77):

dim_code = discriminant // 10000
dimension_map = {
1: 'Length',
2: 'Time',
3: 'Angle',
4: 'Mass',
5: 'Power'
}

Discriminant ranges encode dimensions:

  • 10000-19999 -> Length
  • 20000-29999 -> Time
  • 30000-39999 -> Angle
  • 40000-49999 -> Mass
  • 50000-59999 -> Power

Name Conversion

to_pascal_case() (lines 54-56): Converts UPPER_SNAKE_CASE to PascalCase:

  • KILOMETER -> Kilometer
  • LIGHT_YEAR -> LightYear

Literal Suffix Handling

make_literal_suffix() (lines 106-121): Converts unit symbols to valid C++ literal suffixes:

  • m -> _m
  • km/h -> _km_per_h
  • degree sign -> _deg
  • micrometer (um) -> _um

Collision Detection (lines 131-142): The generator tracks used suffixes and skips duplicates, printing warnings:

Warning: Skipping literal _nm for ... (conflicts with Nanometer)

Header Generation

generate_header_for_dimension() (lines 86-104): Produces a complete header with:

  1. Tag declarations
  2. UnitTraits specializations
  3. Type aliases

Template (lines 14-30):

#pragma once
#include "../ffi_core.hpp"
namespace qtty {
{tag_declarations}
{unit_traits}
{type_aliases}
} // namespace qtty

Invocation

The generator is invoked by CMake as a custom command (see CMakeLists.txt lines 28-40):

add_custom_command(
OUTPUT ${GENERATED_HEADERS}
COMMAND ${CARGO_BIN} run --manifest-path ${CMAKE_CURRENT_SOURCE_DIR}/gen_cpp_units/Cargo.toml --release
-- ${QTTY_UNITS_CSV} ${CMAKE_CURRENT_SOURCE_DIR}/include/qtty
DEPENDS ${QTTY_UNITS_CSV}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating C++ unit wrappers from units.csv (Rust)"
VERBATIM
)

This ensures headers are regenerated whenever the FFI header or generator script changes.

Build System Integration

CMake Build Pipeline

Location: CMakeLists.txt

The build process orchestrates three tools: Python (generation), Cargo (Rust compilation), and CMake (C++ integration).

Build Flow:

1. gen_cpp_units target
- Runs the Rust `gen_cpp_units` tool
- Generates include/qtty/units/*.hpp and literals.hpp
|
v
2. build_qtty_ffi target
- Invokes: cargo build -p qtty-ffi --release
- Produces: libqtty_ffi.{so,dylib,dll}
- Depends on: gen_cpp_units
|
v
3. qtty_ffi IMPORTED library
- Wraps the compiled Rust library
- Sets IMPORTED_LOCATION to the shared library path
|
v
4. qtty_cpp INTERFACE library
- Header-only C++ wrapper
- Includes: include/ and qtty-ffi/include/
- Links: qtty_ffi
|
v
5. demo and test_ffi executables
- Link against qtty_cpp
- RPATH configured for runtime library location

Key CMake Targets

gen_cpp_units (lines 42-49): Custom target that runs the Python generator.

build_qtty_ffi (lines 52-60): Custom target invoking Cargo to build the Rust library. Depends on gen_cpp_units to ensure headers are available.

qtty_ffi (lines 63-68): IMPORTED library representing the compiled Rust shared library.

qtty_cpp (lines 71-77): INTERFACE library (header-only) providing:

  • Include paths for C++ headers
  • Include paths for C FFI headers
  • Link dependency on qtty_ffi

demo and test_ffi (lines 90-96, 108-123): Executable targets linking against qtty_cpp with proper RPATH settings for finding the shared library at runtime.

Platform-Specific Handling

Library Extension (lines 17-25):

if(APPLE)
set(QTTY_LIBRARY_PATH ${QTTY_ARTIFACT_DIR}/libqtty_ffi.dylib)
elseif(WIN32)
set(QTTY_LIBRARY_PATH ${QTTY_ARTIFACT_DIR}/qtty_ffi.dll)
set(QTTY_IMPORT_LIBRARY ${QTTY_ARTIFACT_DIR}/qtty_ffi.dll.lib)
else()
set(QTTY_LIBRARY_PATH ${QTTY_ARTIFACT_DIR}/libqtty_ffi.so)
endif()

RPATH Configuration (lines 79-84):

if(APPLE)
set(_qtty_rpath "@loader_path/../qtty/target/release")
elseif(UNIX)
set(_qtty_rpath "$ORIGIN/../qtty/target/release")
endif()

This ensures the shared library is found at runtime without requiring LD_LIBRARY_PATH or DYLD_LIBRARY_PATH.

Google Test Integration

FetchContent (lines 87-96): CMake automatically downloads Google Test v1.14.0 during configuration.

Test Discovery (line 126):

gtest_discover_tests(test_ffi)

This integrates Google Test with CTest, allowing individual test execution via CTest filters.

Summary

The qtty-cpp architecture achieves:

  1. Type Safety: Compile-time prevention of unit mismatches via templates
  2. Zero Overhead: Header-only design with inline functions
  3. Maintainability: Automatic generation from the authoritative Rust FFI interface
  4. Usability: Natural C++ syntax with operator overloading and literals
  5. Correctness: Runtime validation via the battle-tested Rust qtty library

The three-layer design cleanly separates concerns: Rust handles the physics, Python handles code generation, and C++ templates handle type safety.