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:
- Rust qtty-ffi Layer - The foundational C FFI library exposing unit conversions and quantity operations
- Generated C++ Headers - Automatically generated type-safe wrappers for all supported units
- 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 {};
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:
- include/qtty/units/length.hpp - Length units (Meter, Kilometer, etc.)
- include/qtty/units/time.hpp - Time units (Second, Hour, etc.)
- include/qtty/units/angular.hpp - Angular units (Radian, Degree, etc.)
- include/qtty/units/mass.hpp - Mass units (Kilogram, Gram, etc.)
- include/qtty/units/power.hpp - Power units (Watt, Horsepower, etc.)
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);
Natural Operations: Operator overloading for intuitive syntax
Meter a(10.0), b(5.0);
Meter sum = a + b;
Meter scaled = a * 2.0;
bool greater = a > b;
Safe Conversions: Type-checked unit conversions at runtime
Meter m(1000.0);
Kilometer km = m.
to<Kilometer>();
Quantity< typename ExtractTag< TargetType >::type > to() const
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):
- Create source quantity: qtty_quantity_make(m_value, unit_id(), &src_qty)
- Call conversion: qtty_quantity_convert(src_qty, target_unit_id, &dst_qty)
- Check status: check_status(status, "Converting units")
- 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 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>();
} 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:
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:
- Tag declarations
- UnitTraits specializations
- Type aliases
Template (lines 14-30):
#pragma once
#include "../ffi_core.hpp"
{tag_declarations}
{unit_traits}
{type_aliases}
}
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:
- Type Safety: Compile-time prevention of unit mismatches via templates
- Zero Overhead: Header-only design with inline functions
- Maintainability: Automatic generation from the authoritative Rust FFI interface
- Usability: Natural C++ syntax with operator overloading and literals
- 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.