Omnirefl

A C++ reflection tool built for a seamless experience without macros* or UB.
* Some compatibility QoL before C++17/20 uses macros.

Meme comparing Omnirefl AST parsing and template machinery with languages that have built-in reflection
Obligatory self-reflection meta joke.

Sneak Peek

The complete runnable example implements the visitor and shows how to write reflected fields. Its setup and model are below:

// One .cpp; no declaration headers, metadata files, or reflection macros.

// Example visitor that prints reflected records during a depth-first walk
// of type dependencies. The struct keeps this logic below; an inline
// lambda would also work.
struct print_reachable_records {
  template <omni::record_meta Root>
  void operator()(Root) const;
};

template <typename T>
void print_reflection() {
  // `reflected_call` makes metadata for `T` and its type dependencies available
  // inside the visitor's `operator()` and the functions it calls.
  omni::reflected_call(print_reachable_records{}, omni::type<T>);
}

// Mutating example; its implementation showcases several reflected arguments
// in one `reflected_call`.
template <typename... Values>
void update_fields(Values *...values);

// Omnirefl reflects globally accessible, named records (structs, classes, or
// unions) and enums on demand.

namespace oceanic {
/// Declaration comments are available as reflection metadata.
struct vessel { //< Discovered as the `mapped_type` of `fleet<T>::vessels`.
  // Nested structs are supported recursively inside non-template records.
  struct position { //< Discovered via the `location` field.
    using degrees = double;

    degrees latitude = 0;
    degrees longitude = 0;
  };

  position location;

  // Reflected field properties are queryable.
  mutable std::string name = "before"; //< `.is_mutable()` is true.

  // Public non-static methods are reflected. Their parameter and return types
  // join dependency discovery. Overloaded methods are skipped with a warning.
  // Doxygen parameter descriptions, such as `@param` or `\param[in]`, are
  // available through parameter metadata.

  /** Check proximity.
   * @param other Other position.
   * @return Whether the positions are near.
   */
  bool is_near(position other) const {
    return location.latitude == other.latitude
      && location.longitude == other.longitude;
  }
};

/** Measure a distance.
 * @param scale Multiplier.
 * @return The scaled distance.
 */
double distance(vessel, double scale) {
  return scale;
}

// Primary templates are supported; names appear without template arguments.
template <typename T>
struct fleet { //< Discovered as an `omni::type_t` argument to `reflected_call`.
  // Unsupported nested records emit a warning if discovered as dependencies.
  struct not_supported {};

  // Free functions are reflected only when discovered through a record field
  // using the special `function_t` tag.
  omni::function_t<distance> measure;

  // Standard-library types are not reflected, but their dependency protocols
  // still apply. The specialization for `T` is discovered via `mapped_type`.
  std::map<std::string, T> vessels;
};

struct telemetry { //< Discovered as a value argument to `reflected_call`.
  // Writing bit-fields without UB is supported via `.set_value()`.
  mutable unsigned depth : 10 = 42;
  const unsigned sensor = 108; //< `.is_const()` is true.

  // Only public fields are reflected.
  private:
  [[maybe_unused]] unsigned john_cena = 49; //< can't see
};

} // namespace oceanic

Minimal CMake setup:

cmake_minimum_required(VERSION 3.20 FATAL_ERROR)

project(sneak_peek LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(omnirefl CONFIG REQUIRED)

add_executable(sneak_peek main.cpp)

if(MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    # MinGW's std::print terminal support lives in this library.
    target_link_libraries(sneak_peek PRIVATE stdc++exp)
endif()

# Reflection is not transitive: only this target's own C++ translation units are
# instrumented. Call omni_reflected_target for each target that should be
# reflected.
omni_reflected_target(sneak_peek)

Build the example:

# Builds rerun instrumentation automatically for changed inputs.
# To trigger it manually:
# cmake --build build --target sneak_peek.omni
cmake --build build --target sneak_peek

Run it:

$ ./build/sneak_peek
// Primary templates are supported; names appear without template arguments.
oceanic::fleet {
  measure(vessel, double scale [Multiplier.]) -> double; // Measure a distance.
  // Standard-library types are not reflected, but their dependency protocols
  // still apply. The specialization for `T` is discovered via `mapped_type`.
  vessels: map<std::string, T>;
}
// Declaration comments are available as reflection metadata.
oceanic::vessel {
  location: vessel::position (resolves to vessel::position);
  // `.is_mutable()` is true.
  name: string = before;
  is_near(vessel::position other [Other position.]) -> bool; // Check proximity.
}
// Nested structs are supported recursively inside non-template records.
oceanic::vessel::position {
  latitude: vessel::position::degrees = 0;
  longitude: vessel::position::degrees = 0;
}
before: depth=42 sensor=108 name=before
after:  depth=815 sensor=108 name=oceanic

This example is included in the installed package; see Examples, Tests and Benchmarks.

The comprehensive guide covers the remaining interface and compatibility features.

Seamless Experience

  1. Add omni_reflected_target(...) for the CMake target.
  2. Use omni::reflected_call(...) where reflection is needed.

Everything else remains regular C++. Omnirefl discovers the argument types and supported dependencies, then generates and force-includes their metadata. No macros, compiler-specific UB, or manual regeneration are required.

Types can be declared and reflected directly in the same .cpp. No dedicated declaration headers, schemas, annotations, or checked-in metadata files are required; generated metadata remains a build artifact.

Status

Omnirefl is under active testing and interface polishing. Version 0.1.0 is planned after the initial set of extensions is complete. Until then, interfaces and package layout may change without compatibility guarantees. Release notes aim to call out every breaking interface change.

Supported Scope

Omnirefl reflects the public data surface of named C++ records and enums (see Limitations).

Dependency Protocols

Additional reflected types are discovered through:

Supported public routes may expose otherwise non-public nested dependencies.

Standard-library record types are not traversed as reflectable records outside those protocol routes.

Is It Slow?

Omnirefl uses a Clang frontend action: it preprocesses the translation unit and builds its AST, but does not perform object-code optimization or code generation. The overhead target is roughly the frontend portion of a complete object build: about 30% as an order-of-magnitude expectation. The actual ratio depends on the source, included headers, compiler, and optimization level.

The packaged benchmark baseline is intentionally large enough to represent a meaningful translation unit and contains a reasonable amount of ordinary and reflected code. CI records its reflection and subsequent Release object-build times across benchmarked platforms. See the continuous benchmark and workflow history for observed results.

Only instrumented targets pay this cost, and reflected translation units can be isolated in dedicated targets. The impact is therefore most noticeable during initial generation. Omnirefl emits dependency files for the source and all its included headers, so Ninja reruns instrumentation only when one of those inputs changes.

Install

Install a .deb normally. Unpack a .tar.gz or .zip archive and use its omnirefl-* directory as the installation prefix.

Examples, Tests and Benchmarks

share/omnirefl packages tests, examples, and benchmarks. You may configure the whole directory out of source, or any share/omnirefl/examples/<folder> as a standalone project.

Examples, tests, and benchmarks are enabled by default; disable a category with -DBUILD_EXAMPLES=OFF, -DBUILD_TESTING=OFF, or -DBUILD_BENCHMARKS=OFF. Unsupported examples are skipped with a status message.

Alternatively, download and unpack the Cosmopolitan archive from the latest release into ./omnirefl:

mkdir -p omnirefl
curl -fL "$(curl -fsSL \
  https://api.github.com/repos/sergio-eld/omnirefl/releases/latest \
  | sed -n 's/.*"browser_download_url": "\(.*cosmo-universal.*\.tar\.gz\)".*/\1/p')" \
  | tar -xz -C omnirefl --strip-components=1
# Use /usr when installed system-wide from a Debian package.
prefix="$PWD/omnirefl"

mkdir build && cd build
cmake "$prefix/share/omnirefl" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  "-DCMAKE_PREFIX_PATH=$prefix"
cmake --build .
ctest --timeout 600 --output-on-failure --no-tests=error

Each share/omnirefl/examples/<folder> directory is self-contained. Configure one independently, such as share/omnirefl/examples/sneak_peek, or copy it into your own workspace. The Sneak Peek requires CMake 3.20 and a configured C++23 toolchain.

Extension examples, tests, and benchmarks are grouped under share/omnirefl/extensions:

Limitations

Several declaration-shape constraints below follow from the generated-header model: reflected types must be nameable before their source declarations. See How It Works.

How It Works

reflected_call identifies root records and enums. Omnirefl walks their public dependency protocols, then force-includes a generated header before the translation unit.

The generated header is an internal, per-translation-unit build artifact. It is not intended to be installed or published as a reusable interface: its metadata reflects the exact compiler invocation, including preprocessor definitions, language and target flags, and include paths. The same source may therefore produce different metadata in another target or project. The header does not #include user declaration headers or reproduce their definitions.

Earlier iterations attempted to reconstruct the required user includes, but that becomes a separate build-integration problem: a declaration may live only in a .cpp, a third-party header's supported include path may differ from its filesystem path, and project headers may rely on transitive includes or a particular include order.

The current design avoids guessing. It forward-declares namespace-scope roots where C++ permits it; not every type can be forward-declared (see Limitations). Field access remains dependent on a template parameter, delaying instantiation until the source definition is available. Nested-type lookup uses the same mechanism through SFINAE. A simplified generated shape is:

namespace app {
struct root; // The definition may remain in the translation unit.
}

namespace omni {
namespace detail {

// Field accessors use T, so their instantiation is delayed until app::root is
// complete.
template <typename T>
struct _reflected<struct app::root, T> {
  // Internal discovery hook for the reflected C++ type.
  using type = T;

  // Metadata omitted.
};

// _wrt means "with respect to": its type is app::root, but remains
// syntactically dependent on T so nested-name lookup is delayed.
template <typename T>
struct _reflected<T,
  typename std::enable_if<
    std::is_same<T, typename _wrt<app::root, T>::type::nested>::value,
    T>::type> {
  // Internal discovery hook for the reflected C++ type.
  using type = T;

  // Metadata omitted.
};

} // namespace detail
} // namespace omni

This model also defines the declaration boundary. Generated code can reproduce ordinary record and enum forward declarations and defer nested lookup, but it cannot safely recreate local or unnamed types, non-forward-declarable enums, or records nested in template records. Constrained primary templates and explicit or partial specializations are also unsupported.

Functions have no equivalent general forward-declaration strategy: reproducing a declaration requires its parameter types and overload identity, which may not be nameable before the source declaration. Therefore function_t is not a standalone reflection root. The first implementation discovers it only as a special field member of a reflected record, where generated access can remain dependent on the owning record. The function's parameter and return types then enter dependency discovery on a best-effort basis.