Skip to content

C++ sugar

caliper.hpp is the header-only, optional C++ layer over the frozen C ABI (PLATFORM.md §8). A C applet can implement abi.h by hand; the sugar exists so C++ authors write a class and a macro instead of five extern "C" bridge functions and a hand-built descriptor. It requires C++20 (the CALIPER_APPLET macro uses designated initializers).

The CALIPER_APPLET macro

You write a class deriving from caliper::Applet and invoke the macro once:

#include <caliper/caliper.hpp>

class MyApplet final : public caliper::Applet {
public:
    bool on_init(caliper::Host& host) override { host.log_info("hi"); return true; }
    void on_frame(const caliper::Frame& f) override { /* draw */ }
    void on_cleanup() override {}
};

CALIPER_APPLET(MyApplet,
    .id       = "dev.example.myapplet",
    .version  = "0.1.0",
    .name     = "My Applet",
    .summary  = "one-line description",
    .tag      = "Demo",
    .services = {CALIPER_LOG_V1, CALIPER_UI_V1})

Required field order

The fields are C++20 designated initializers on caliper::AppletMeta, and the order is fixed — the aggregate is initialized positionally, so the names are documentation, not a reordering license:

  1. .id — reverse-DNS identifier, must match caliper.toml
  2. .version — applet semver, must match the manifest
  3. .name — human-facing title
  4. .summary — one-line description
  5. .tag — category label
  6. .services — brace-list of required service-id macros (e.g. {CALIPER_LOG_V1})

.services is a const char*[15]; any trailing slots are zero-initialized, so the array the host sees is NULL-terminated automatically. Fifteen slots is the fixed capacity — list only the services your applet truly requires (negotiation refuses to load an applet whose required services the host cannot vend).

What the macro generates

  • One static const caliper::AppletMeta kMeta holding the six fields.
  • A Holder struct pairing your class instance with a caliper::Host.
  • Five exception-safe extern "C" bridge functions — create, destroy, initialize, frame, cleanup — that own the try/catch so no C++ exception ever crosses the C boundary (a throw is caught, logged via host.log_error("unhandled exception in on_<phase>"), and swallowed; create returns nullptr on a failed new).
  • caliper::ui::connect(host) is called inside initialize, before on_init, so ImGui/ImPlot contexts are live by the time your code runs.
  • The single exported symbol caliper_applet_descriptor() returning a static const CaliperAppletDescriptor wired to kMeta and the five bridges.

One per dylib

The macro defines the caliper_applet_gen namespace and the exported caliper_applet_descriptor symbol, so exactly one CALIPER_APPLET may appear per shared library (one applet per dylib — the ABI contract). In a test binary that needs to exercise several behaviours, keep a single CALIPER_APPLET and toggle behaviour from inside the class (see the fixture-host recipe below).

ui::connect semantics

caliper::ui::connect(const CaliperHost*) performs the ImGui context + allocator handoff described in PLATFORM.md §6d, in the one order authors get wrong when they do it by hand:

  1. Fetches caliper.ui.v1 via get_service. If the host does not vend it (headless), connect returns false and does nothing — this is why the fixture host and other headless drivers work without a GL context.
  2. Installs the host's allocator pair with ImGui::SetAllocatorFunctions, so every allocation in the applet's copy of ImGui lands on the host heap (the crux of sound context-sharing across a DLL boundary).
  3. Calls ImGui::SetCurrentContext, ImPlot::SetCurrentContext, and ImPlot3D::SetCurrentContext with the host-owned contexts.

Because the macro calls connect for you inside initialize, most applets never call it directly.

Fixture-host TDD recipe

caliper::testing::FixtureHost (from <caliper/fixture_host.h>, target caliper::sdk_testing) is a headless fake CaliperHost for test-driving applets and sugar without launching UI. It vends caliper.log.v1 only; get_service returns NULL for everything else (so ui::connect no-ops). It records every logged line, exposed via log_lines() and log_contains().

Because the C ABI carries no user-data pointer, the log/service thunks route through a single static active pointer: exactly one FixtureHost may be live per process at a time (construct one per TEST_CASE; its destructor clears the active slot).

Drive an applet through the generated C table exactly as the host would:

#include <doctest/doctest.h>
#include <caliper/caliper.hpp>
#include <caliper/fixture_host.h>

TEST_CASE("applet: initialize logs and frame is exception-safe") {
    caliper::testing::FixtureHost fx;
    const CaliperAppletDescriptor* d = caliper_applet_descriptor();

    void* self = d->api.create();
    REQUIRE(self != nullptr);
    REQUIRE(d->api.initialize(self, fx.host()));   // runs on_init
    CHECK(fx.log_contains("hi"));

    CaliperFrameInfo fi{};
    fi.struct_size = sizeof fi;
    fi.fb_width = 640; fi.fb_height = 480; fi.dpi_scale = 2.0f;
    d->api.frame(self, &fi);                        // runs on_frame

    d->api.cleanup(self);
    d->api.destroy(self);
}

Full source

#pragma once
// Caliper C++ sugar (PLATFORM.md §8). Header-only, optional by design: a C
// applet can implement abi.h by hand. Requires C++20 (designated inits).
#include <caliper/abi.h>
#include <caliper/services/log_v1.h>
#include <caliper/services/ui_v1.h>
#include <caliper/services/jobs_v1.h>
#include <caliper/services/device_v1.h>
#include <caliper/services/metrics_v1.h>
#include <caliper/services/artifacts_v1.h>
#include <caliper/services/data_v1.h>
#include <caliper/services/tensor_bridge_v1.h>
#include <caliper/services/tensor_bridge_v1_1.h>
#include <caliper/services/tensor_bridge_v1_2.h>
#include <caliper/services/geometry_v1.h>
#include <caliper/services/geometry_v1_1.h>
#include <caliper/services/geometry_v1_2.h>
#include <caliper/services/geometry_v1_3.h>
#include <caliper/services/export_v1.h>

#include <imgui.h>
#include <implot.h>
#include <implot3d.h>

#include <string>
#include <vector>

namespace caliper {

struct Frame {
    int32_t fb_width = 0, fb_height = 0;   // PHYSICAL pixels (§6a)
    float   dpi_scale = 1.0f;
    double  time_sec = 0.0, delta_sec = 0.0;
    static Frame from(const CaliperFrameInfo& fi) {
        Frame f;
        f.fb_width = fi.fb_width;  f.fb_height = fi.fb_height;
        f.dpi_scale = fi.dpi_scale;
        f.time_sec = fi.time_sec;  f.delta_sec = fi.delta_sec;
        return f;
    }
};

class Host {
public:
    Host() = default;
    explicit Host(const CaliperHost* raw) : raw_(raw) {
        if (raw_ && raw_->get_service)
            log_ = static_cast<const CaliperLogV1*>(
                raw_->get_service(raw_, CALIPER_LOG_V1));
    }
    const CaliperHost* raw() const { return raw_; }
    const void* service(const char* id) const {
        return (raw_ && raw_->get_service) ? raw_->get_service(raw_, id) : nullptr;
    }
    const char* data_dir() const {
        return (raw_ && raw_->applet_data_dir) ? raw_->applet_data_dir : "";
    }
    void log(CaliperLogLevel lv, const char* msg) const {
        if (log_ && log_->log) log_->log(lv, msg);
    }
    void log_info(const char* m) const  { log(CALIPER_LOG_INFO, m); }
    void log_error(const char* m) const { log(CALIPER_LOG_ERROR, m); }

private:
    const CaliperHost* raw_ = nullptr;
    const CaliperLogV1* log_ = nullptr;
};

// Typed wrapper over caliper.jobs.v1 (§7.5). Falsy when the host doesn't vend
// the service; every method null-guards its fn pointer so it stays inert (not
// UB) on a headless/older host. Job fns run UNGUARDED on host worker threads.
class Jobs {
public:
    Jobs() = default;
    explicit Jobs(const Host& host)
        : t_(static_cast<const CaliperJobsV1*>(host.service(CALIPER_JOBS_V1))) {}
    explicit operator bool() const { return t_ && t_->submit; }
    uint64_t submit(const char* label, CaliperJobFn fn, void* user) const {
        return (t_ && t_->submit) ? t_->submit(label, fn, user) : 0;
    }
    void request_cancel(uint64_t id) const {
        if (t_ && t_->request_cancel) t_->request_cancel(id);
    }
    bool is_running(uint64_t id) const {
        return (t_ && t_->is_running) ? t_->is_running(id) : false;
    }
    float progress_of(uint64_t id) const {
        return (t_ && t_->progress_of) ? t_->progress_of(id) : 0.0f;
    }
private:
    const CaliperJobsV1* t_ = nullptr;
};

// Typed wrapper over caliper.metrics.v1 (§7.6). Falsy when the host doesn't
// vend the service; every method null-guards its fn pointer so it stays inert
// (not UB) on a headless/older host. The writers are callable from job threads
// (the host serializes internally). image() takes a CPU-resident HWC u8 tensor;
// a non-conforming tensor is dropped by the host thunk (see host_services.cpp).
class Metrics {
public:
    Metrics() = default;
    explicit Metrics(const Host& host)
        : t_(static_cast<const CaliperMetricsV1*>(host.service(CALIPER_METRICS_V1))) {}
    explicit operator bool() const { return t_ && t_->begin_run; }
    uint64_t begin_run(const char* experiment, const char* run_name) const {
        return (t_ && t_->begin_run) ? t_->begin_run(experiment, run_name) : 0;
    }
    void end_run(uint64_t run) const {
        if (t_ && t_->end_run) t_->end_run(run);
    }
    void scalar(uint64_t run, const char* tag, int64_t step, double value) const {
        if (t_ && t_->scalar) t_->scalar(run, tag, step, value);
    }
    void histogram(uint64_t run, const char* tag, int64_t step,
                   const float* values, int64_t count) const {
        if (t_ && t_->histogram) t_->histogram(run, tag, step, values, count);
    }
    void image(uint64_t run, const char* tag, int64_t step,
               const CaliperTensor* hwc_u8) const {
        if (t_ && t_->image) t_->image(run, tag, step, hwc_u8);
    }
    void hparams_json(uint64_t run, const char* json_utf8) const {
        if (t_ && t_->hparams_json) t_->hparams_json(run, json_utf8);
    }
private:
    const CaliperMetricsV1* t_ = nullptr;
};

// Typed wrapper over caliper.artifacts.v1 (§7.8): content-addressed checkpoint
// storage. Falsy when the host doesn't vend the service; every call is inert
// then (put returns "", path_of nullptr, exists false).
class Artifacts {
public:
    Artifacts() = default;
    explicit Artifacts(const Host& host)
        : t_(static_cast<const CaliperArtifactsV1*>(
              host.service(CALIPER_ARTIFACTS_V1))) {}
    explicit operator bool() const { return t_ && t_->put; }
    // Returns the 64-hex digest, or "" on failure/absence.
    std::string put(const char* name, const void* bytes, uint64_t len,
                    uint64_t run = 0) const {
        if (!(t_ && t_->put)) return {};
        char digest[65] = {};
        return t_->put(name, bytes, len, run, digest) ? std::string(digest)
                                                      : std::string();
    }
    // Host-owned string, valid until the next artifacts.v1 call; nullptr if
    // unknown or the service is absent.
    const char* path_of(const char* digest_or_name) const {
        return (t_ && t_->path_of) ? t_->path_of(digest_or_name) : nullptr;
    }
    bool exists(const char* digest_or_name) const {
        return t_ && t_->exists && t_->exists(digest_or_name);
    }
private:
    const CaliperArtifactsV1* t_ = nullptr;
};

// Typed wrapper over caliper.data.v1 (§7.7): SQL in, Arrow streams out.
// Falsy-inert when absent. The raw stream API is fully exposed; the
// drain_numeric helper covers the common all-numeric-columns case (each
// column widened to double) so simple consumers never touch Arrow buffers.
class Data {
public:
    Data() = default;
    explicit Data(const Host& host)
        : t_(static_cast<const CaliperDataV1*>(
              host.service(CALIPER_DATA_V1))) {}
    explicit operator bool() const { return t_ && t_->query; }
    bool query(const char* sql, ArrowArrayStream* out) const {
        return t_ && t_->query && t_->query(sql, out);
    }
    bool register_dataset(const char* name, const char* uri) const {
        return t_ && t_->register_dataset && t_->register_dataset(name, uri);
    }
    bool open_dataset(const char* name, ArrowArrayStream* out) const {
        return t_ && t_->open_dataset && t_->open_dataset(name, out);
    }
    const char* last_error() const {
        return (t_ && t_->last_error) ? t_->last_error()
                                      : "data.v1 is not available";
    }

    // Drain a stream of numeric columns (int8..int64 / uint8..uint64 /
    // float / double / bool) into column-major doubles. Releases the stream.
    // Returns false (and releases what it took) on a non-numeric column or
    // stream error. NULL values read as whatever the producer left in the
    // data buffer — callers that care about NULLs should walk the raw stream.
    static bool drain_numeric(ArrowArrayStream* stream,
                              std::vector<std::string>* names,
                              std::vector<std::vector<double>>* cols) {
        if (!stream || !stream->get_schema || !cols) return false;
        ArrowSchema schema = {};
        if (stream->get_schema(stream, &schema) != 0) {
            stream->release(stream);
            return false;
        }
        const int64_t n = schema.n_children;
        std::vector<char> fmt(static_cast<size_t>(n < 0 ? 0 : n), 0);
        cols->assign(fmt.size(), {});
        if (names) names->assign(fmt.size(), "");
        bool ok = true;
        for (int64_t c = 0; c < n; c++) {
            const char* f = schema.children[c]->format;
            // Arrow primitive format strings: c/C s/S i/I l/L = ints,
            // f/g = float32/64, b = bool. Anything else is non-numeric.
            const bool numeric = f && f[0] != '\0' && f[1] == '\0' &&
                                 std::string("cCsSiIlLfgb").find(f[0]) !=
                                     std::string::npos;
            if (!numeric) ok = false;
            fmt[static_cast<size_t>(c)] = numeric ? f[0] : 0;
            if (names && schema.children[c]->name)
                (*names)[static_cast<size_t>(c)] = schema.children[c]->name;
        }
        if (schema.release) schema.release(&schema);
        while (ok) {
            ArrowArray array = {};
            if (stream->get_next(stream, &array) != 0) { ok = false; break; }
            if (!array.release) break;  // end of stream
            for (int64_t c = 0; ok && c < array.n_children &&
                                c < static_cast<int64_t>(cols->size()); c++) {
                const ArrowArray* col = array.children[c];
                const void* buf = col->buffers[1];
                auto& out = (*cols)[static_cast<size_t>(c)];
                for (int64_t i = 0; i < array.length; i++) {
                    const int64_t at = col->offset + i;
                    switch (fmt[static_cast<size_t>(c)]) {
                        case 'c': out.push_back(static_cast<const int8_t*>(buf)[at]); break;
                        case 'C': out.push_back(static_cast<const uint8_t*>(buf)[at]); break;
                        case 's': out.push_back(static_cast<const int16_t*>(buf)[at]); break;
                        case 'S': out.push_back(static_cast<const uint16_t*>(buf)[at]); break;
                        case 'i': out.push_back(static_cast<const int32_t*>(buf)[at]); break;
                        case 'I': out.push_back(static_cast<const uint32_t*>(buf)[at]); break;
                        case 'l': out.push_back(static_cast<double>(static_cast<const int64_t*>(buf)[at])); break;
                        case 'L': out.push_back(static_cast<double>(static_cast<const uint64_t*>(buf)[at])); break;
                        case 'f': out.push_back(static_cast<const float*>(buf)[at]); break;
                        case 'g': out.push_back(static_cast<const double*>(buf)[at]); break;
                        case 'b': {  // bit-packed booleans
                            const uint8_t* bits = static_cast<const uint8_t*>(buf);
                            out.push_back((bits[at / 8] >> (at % 8)) & 1);
                            break;
                        }
                        default: ok = false; break;
                    }
                }
            }
            array.release(&array);
        }
        if (stream->release) stream->release(stream);
        return ok;
    }

private:
    const CaliperDataV1* t_ = nullptr;
};

// Typed wrapper over caliper.tensor_bridge.v1 (§7.4): a CaliperTensor becomes a
// live texture, crossing the ABI as an opaque CaliperTextureId. Falsy when the
// host doesn't vend the service; every method null-guards its fn pointer so it
// stays inert (not UB) on a headless/older host. FRAME-THREAD ONLY in v1: a
// tensor produced by a background job is consumed at frame time, so call these
// on the UI/frame thread, never from a job worker (the C8 pattern).
class Bridge {
public:
    Bridge() = default;
    explicit Bridge(const Host& host)
        : t_(static_cast<const CaliperTensorBridgeV1*>(
              host.service(CALIPER_TENSOR_BRIDGE_V1))),
          t11_(static_cast<const CaliperTensorBridgeV1_1*>(
              host.service(CALIPER_TENSOR_BRIDGE_V1_1))),
          t12_(static_cast<const CaliperTensorBridgeV1_2*>(
              host.service(CALIPER_TENSOR_BRIDGE_V1_2))) {}
    explicit operator bool() const { return t_ && t_->texture_from_tensor; }
    CaliperTextureId texture_from_tensor(const CaliperTensor* t,
                                         uint32_t flags = 0) const {
        return (t_ && t_->texture_from_tensor)
            ? t_->texture_from_tensor(t, flags) : 0;
    }
    bool update_texture(CaliperTextureId tex, const CaliperTensor* t) const {
        return (t_ && t_->update_texture) ? t_->update_texture(tex, t) : false;
    }
    void release_texture(CaliperTextureId tex) const {
        if (t_ && t_->release_texture) t_->release_texture(tex);
    }
    CaliperTextureId texture_from_tensor_mapped(const CaliperTensor* t,
                                                int32_t colormap, float vmin,
                                                float vmax,
                                                uint32_t flags = 0) const {
        return (t_ && t_->texture_from_tensor_mapped)
            ? t_->texture_from_tensor_mapped(t, colormap, vmin, vmax, flags) : 0;
    }
    bool alloc_shared(CaliperDType dtype, int32_t ndim, const int64_t* shape,
                      CaliperTensor* out_tensor,
                      CaliperTextureId* out_texture) const {
        return (t_ && t_->alloc_shared)
            ? t_->alloc_shared(dtype, ndim, shape, out_tensor, out_texture)
            : false;
    }
    void free_shared(CaliperTextureId tex) const {
        if (t_ && t_->free_shared) t_->free_shared(tex);
    }
    // v1.1 capability bits — 0 on a v1-only or headless host (D24). Query
    // once per handoff site and pass to adapters::stream_to_tensor; bit
    // CALIPER_BRIDGE_CAP_STREAM_ORDERED means a non-NULL CaliperTensor.stream
    // replaces the adapter's device drain.
    uint32_t caps() const { return (t11_ && t11_->caps) ? t11_->caps() : 0u; }
    // v1.2 imported-allocation ops — inert (0/false/no-op) on a host without
    // the v1_2 service or the CALIPER_BRIDGE_CAP_IMPORT_ALLOC bit (D24). Hand
    // import_allocation an OS shareable handle from cuMemExportToShareableHandle;
    // a 0 return means the applet stays on the v1 D2D-copy path.
    CaliperAllocId import_allocation(void* h, uint64_t size, uint32_t type) const {
        return (t12_ && t12_->import_allocation)
                   ? t12_->import_allocation(h, size, type) : 0;
    }
    void release_allocation(CaliperAllocId a) const {
        if (t12_ && t12_->release_allocation) t12_->release_allocation(a);
    }
    bool update_texture_from_alloc(CaliperTextureId tex, CaliperAllocId a,
                                   uint64_t off, const CaliperTensor* d) const {
        return (t12_ && t12_->update_texture_from_alloc)
                   ? t12_->update_texture_from_alloc(tex, a, off, d) : false;
    }
    // Opaque id -> ImTextureID for ImGui::Image (§5.4: the id's value IS the
    // host's ImGui-compatible handle for this backend; applets never interpret
    // it, they only cast it here).
    static ImTextureID imtex(CaliperTextureId tex) { return (ImTextureID)tex; }
private:
    const CaliperTensorBridgeV1* t_ = nullptr;
    const CaliperTensorBridgeV1_1* t11_ = nullptr;
    const CaliperTensorBridgeV1_2* t12_ = nullptr;
};

// Typed wrapper over caliper.geometry.v1: instanced 3-D points drawn directly
// from an imported allocation (bridge v1.2) into an offscreen view texture.
// Falsy when the host doesn't vend the service; every method null-guards so
// it stays inert on hosts without the geometry path (D24). FRAME-THREAD ONLY,
// same as Bridge.
class Geometry {
public:
    Geometry() = default;
    explicit Geometry(const Host& host)
        : g_(static_cast<const CaliperGeometryV1*>(
              host.service(CALIPER_GEOMETRY_V1))),
          g11_(static_cast<const CaliperGeometryV1_1*>(
              host.service(CALIPER_GEOMETRY_V1_1))),
          g12_(static_cast<const CaliperGeometryV1_2*>(
              host.service(CALIPER_GEOMETRY_V1_2))),
          g13_(static_cast<const CaliperGeometryV1_3*>(
              host.service(CALIPER_GEOMETRY_V1_3))) {
        if (!g12_ && g13_) {
            // v1_3-only host: the tables are slot-identical, but the v1.3
            // draw_primitives entry enforces min stride 256 — v1.1/v1.2-shaped
            // draws must be widened to zero-tailed 256-byte records. Chains
            // BEFORE the v1_2 tier so both flags set on a v1_3-only host.
            g12_ = reinterpret_cast<const CaliperGeometryV1_2*>(g13_);
            widen_v12_draws_ = true;
        }
        if (!g11_ && g12_) {
            // v1_2-only host: the tables are slot-identical, but the v1.2
            // draw_primitives entry enforces min stride 216 — v1.1-shaped
            // draws must be widened (Task 1, hardening plan 2026-07-10).
            g11_ = reinterpret_cast<const CaliperGeometryV1_1*>(g12_);
            widen_v11_draws_ = true;
        }
        if (!g_ && g11_) {
            g_ = reinterpret_cast<const CaliperGeometryV1*>(g11_);
        }
    }

    explicit operator bool() const { return g_ != nullptr; }

    uint32_t caps() const { return (g_ && g_->caps) ? g_->caps() : 0u; }
    bool has_primitives() const {
        return (caps() & CALIPER_GEOM_CAP_PRIMITIVES) != 0u;
    }
    bool has_textured() const {
        return (caps() & CALIPER_GEOM_CAP_TEXTURED) != 0u;
    }
    bool has_instanced() const {
        return (caps() & CALIPER_GEOM_CAP_INSTANCED) != 0u;
    }

    CaliperTextureId create_view(uint32_t w, uint32_t h) const {
        return (g_ && g_->create_view) ? g_->create_view(w, h) : 0;
    }
    CaliperTextureId create_view_ex(uint32_t w, uint32_t h,
                                    uint32_t flags) const {
        return (g11_ && g11_->create_view_ex)
                   ? g11_->create_view_ex(w, h, flags) : 0;
    }
    void release_view(CaliperTextureId view) const {
        if (g_ && g_->release_view) g_->release_view(view);
    }
    bool draw_points(CaliperTextureId view, const CaliperGeomCamera* cam,
                     CaliperAllocId pos_alloc, uint64_t pos_offset,
                     uint64_t count,
                     CaliperAllocId attr_alloc, uint64_t attr_offset,
                     int32_t colormap, float vmin, float vmax,
                     float size_px, uint32_t clear_rgba) const {
        return (g_ && g_->draw_points)
                   ? g_->draw_points(view, cam, pos_alloc, pos_offset, count,
                                     attr_alloc, attr_offset, colormap, vmin,
                                     vmax, size_px, clear_rgba)
                   : false;
    }
    bool draw_primitives(CaliperTextureId view, const CaliperGeomCamera& cam,
                         const CaliperGeomDraw* draws, uint32_t count,
                         uint32_t clear_rgba) const {
        if (!g11_ || !g11_->draw_primitives) return false;
        if (widen_v12_draws_) {
            // v1_3-only host: widen each frozen 192-byte record straight to a
            // zero-tailed 256-byte v1.3 record (the widest required).
            std::vector<CaliperGeomDrawV1_3> wide(count);
            for (uint32_t i = 0; i < count; ++i) wide[i].base.base = draws[i];
            return g13_->draw_primitives(view, &cam, wide.data(), count,
                                         sizeof(CaliperGeomDrawV1_3), clear_rgba);
        }
        if (widen_v11_draws_) {
            // Widen each frozen 192-byte record into a zero-tailed v1.2 record.
            std::vector<CaliperGeomDrawV1_2> wide(count);
            for (uint32_t i = 0; i < count; ++i) wide[i].base = draws[i];
            return g12_->draw_primitives(view, &cam, wide.data(), count,
                                         sizeof(CaliperGeomDrawV1_2), clear_rgba);
        }
        return g11_->draw_primitives(view, &cam, draws, count,
                                     sizeof(CaliperGeomDraw), clear_rgba);
    }
    bool draw_primitives(CaliperTextureId view, const CaliperGeomCamera& cam,
                         const CaliperGeomDrawV1_2* draws, uint32_t count,
                         uint32_t clear_rgba) const {
        if (!g12_ || !g12_->draw_primitives) return false;
        if (widen_v12_draws_) {
            // v1_3-only host: widen each 216-byte record into a zero-tailed
            // 256-byte v1.3 record.
            std::vector<CaliperGeomDrawV1_3> wide(count);
            for (uint32_t i = 0; i < count; ++i) wide[i].base = draws[i];
            return g13_->draw_primitives(view, &cam, wide.data(), count,
                                         sizeof(CaliperGeomDrawV1_3), clear_rgba);
        }
        return g12_->draw_primitives(view, &cam, draws, count,
                                     sizeof(CaliperGeomDrawV1_2), clear_rgba);
    }
    bool draw_primitives(CaliperTextureId view, const CaliperGeomCamera& cam,
                         const CaliperGeomDrawV1_3* draws, uint32_t count,
                         uint32_t clear_rgba) const {
        return (g13_ && g13_->draw_primitives)
                   ? g13_->draw_primitives(view, &cam, draws, count,
                                           sizeof(CaliperGeomDrawV1_3),
                                           clear_rgba)
                   : false;
    }
private:
    const CaliperGeometryV1* g_ = nullptr;
    const CaliperGeometryV1_1* g11_ = nullptr;
    const CaliperGeometryV1_2* g12_ = nullptr;
    const CaliperGeometryV1_3* g13_ = nullptr;
    bool widen_v11_draws_ = false;
    bool widen_v12_draws_ = false;
};

inline CaliperGeomDraw geom_draw_defaults() {
    CaliperGeomDraw d{};
    d.flat_rgba = 0xffffffffu;
    d.vmin = 0.0f;
    d.vmax = 1.0f;
    d.size_px = 1.0f;
    d.model[0] = 1.0f;
    d.model[5] = 1.0f;
    d.model[10] = 1.0f;
    d.model[15] = 1.0f;
    return d;
}

inline CaliperGeomDrawV1_2 geom_draw_v1_2_defaults() {
    CaliperGeomDrawV1_2 d{};
    d.base = geom_draw_defaults();
    return d;
}

inline CaliperGeomDrawV1_3 geom_draw_v1_3_defaults() {
    CaliperGeomDrawV1_3 d{};
    d.base = geom_draw_v1_2_defaults();
    return d;   // zero instance tail -> non-instanced (additive default)
}

// caliper.export.v1 sugar (PUBLISHING.md §3). Mirrors caliper::Geometry's
// draw_primitives overload set: the export ABI carries only v1.3 records, so
// v1.1/v1.2-shaped draws are widened to zero-tailed 256-byte records with the
// SAME idiom Geometry uses (base.base = ...), then passed at stride 256. The
// caller hands the same arrays it draws with; state_json is copied verbatim
// into the sidecar (nullptr -> null).
class Export {
public:
    Export() = default;
    explicit Export(const Host& host)
        : e_(static_cast<const CaliperExportV1*>(
              host.service(CALIPER_EXPORT_V1))) {}

    explicit operator bool() const { return e_ != nullptr; }

    uint32_t caps() const { return (e_ && e_->caps) ? e_->caps() : 0u; }
    bool has_view_png() const {
        return (caps() & CALIPER_EXPORT_CAP_VIEW_PNG) != 0u;
    }

    // v1.3 records: passed straight through at stride 256 (no copy).
    bool view_png(const char* path, uint32_t w, uint32_t h,
                  const CaliperGeomCamera& cam,
                  const CaliperGeomDrawV1_3* draws, uint32_t count,
                  uint32_t clear_rgba, const char* state_json = nullptr) const {
        return (e_ && e_->view_png)
                   ? e_->view_png(path, w, h, &cam, draws, count,
                                  sizeof(CaliperGeomDrawV1_3), clear_rgba,
                                  state_json) != 0u
                   : false;
    }
    // v1.2 records: widen each into a zero-tailed 256-byte v1.3 record.
    bool view_png(const char* path, uint32_t w, uint32_t h,
                  const CaliperGeomCamera& cam,
                  const CaliperGeomDrawV1_2* draws, uint32_t count,
                  uint32_t clear_rgba, const char* state_json = nullptr) const {
        if (!e_ || !e_->view_png) return false;
        std::vector<CaliperGeomDrawV1_3> wide(count);
        for (uint32_t i = 0; i < count; ++i) wide[i].base = draws[i];
        return e_->view_png(path, w, h, &cam, wide.data(), count,
                            sizeof(CaliperGeomDrawV1_3), clear_rgba,
                            state_json) != 0u;
    }
    // v1.1 records: widen the frozen 192-byte prefix straight to v1.3.
    bool view_png(const char* path, uint32_t w, uint32_t h,
                  const CaliperGeomCamera& cam,
                  const CaliperGeomDraw* draws, uint32_t count,
                  uint32_t clear_rgba, const char* state_json = nullptr) const {
        if (!e_ || !e_->view_png) return false;
        std::vector<CaliperGeomDrawV1_3> wide(count);
        for (uint32_t i = 0; i < count; ++i) wide[i].base.base = draws[i];
        return e_->view_png(path, w, h, &cam, wide.data(), count,
                            sizeof(CaliperGeomDrawV1_3), clear_rgba,
                            state_json) != 0u;
    }

    // Frame sequences (0 handle = refusal).
    uint64_t begin_sequence(const char* dir, uint32_t w, uint32_t h,
                            const char* state_json = nullptr) const {
        return (e_ && e_->begin_sequence)
                   ? e_->begin_sequence(dir, w, h, state_json) : 0u;
    }
    bool frame(uint64_t seq, const CaliperGeomCamera& cam,
               const CaliperGeomDrawV1_3* draws, uint32_t count,
               uint32_t clear_rgba) const {
        return (e_ && e_->frame)
                   ? e_->frame(seq, &cam, draws, count,
                               sizeof(CaliperGeomDrawV1_3), clear_rgba) != 0u
                   : false;
    }
    bool frame(uint64_t seq, const CaliperGeomCamera& cam,
               const CaliperGeomDrawV1_2* draws, uint32_t count,
               uint32_t clear_rgba) const {
        if (!e_ || !e_->frame) return false;
        std::vector<CaliperGeomDrawV1_3> wide(count);
        for (uint32_t i = 0; i < count; ++i) wide[i].base = draws[i];
        return e_->frame(seq, &cam, wide.data(), count,
                         sizeof(CaliperGeomDrawV1_3), clear_rgba) != 0u;
    }
    bool frame(uint64_t seq, const CaliperGeomCamera& cam,
               const CaliperGeomDraw* draws, uint32_t count,
               uint32_t clear_rgba) const {
        if (!e_ || !e_->frame) return false;
        std::vector<CaliperGeomDrawV1_3> wide(count);
        for (uint32_t i = 0; i < count; ++i) wide[i].base.base = draws[i];
        return e_->frame(seq, &cam, wide.data(), count,
                         sizeof(CaliperGeomDrawV1_3), clear_rgba) != 0u;
    }
    void end_sequence(uint64_t seq) const {
        if (e_ && e_->end_sequence) e_->end_sequence(seq);
    }
private:
    const CaliperExportV1* e_ = nullptr;
};

// Snapshot of caliper.device.v1 (§7.3). Defaults to CPU when the host doesn't
// vend the service; name is host-owned (valid for the process lifetime).
struct Device {
    CaliperDeviceKind kind = CALIPER_DEV_CPU;
    int32_t index = 0;
    const char* name = "CPU";              // host-owned string
    uint64_t free_memory_hint = 0;
    static Device query(const Host& host) {
        Device d;
        auto* t = static_cast<const CaliperDeviceV1*>(
            host.service(CALIPER_DEVICE_V1));
        if (t && t->kind) {
            d.kind = t->kind();
            d.index = t->index ? t->index() : 0;
            d.name = t->name ? t->name() : "";
            d.free_memory_hint = t->free_memory_hint ? t->free_memory_hint() : 0;
        }
        return d;
    }
};

class Applet {
public:
    virtual ~Applet() = default;
    virtual bool on_init(Host& host) = 0;
    virtual void on_frame(const Frame& frame) = 0;
    virtual void on_cleanup() {}
};

namespace ui {
// SetAllocatorFunctions + SetCurrentContext x3, in one call authors cannot
// get wrong (§6d). Returns false when the host has no ui.v1 (headless).
inline bool connect(const CaliperHost* h) {
    if (!h || !h->get_service) return false;
    auto* ui = static_cast<const CaliperUiV1*>(h->get_service(h, CALIPER_UI_V1));
    if (!ui) return false;
    CaliperImGuiAllocFn alloc = nullptr;
    CaliperImGuiFreeFn  free_fn = nullptr;
    void* user = nullptr;
    ui->imgui_allocators(&alloc, &free_fn, &user);
    if (alloc && free_fn)
        ImGui::SetAllocatorFunctions(reinterpret_cast<ImGuiMemAllocFunc>(alloc),
                                     reinterpret_cast<ImGuiMemFreeFunc>(free_fn),
                                     user);
    ImGui::SetCurrentContext(ui->imgui_context());
    ImPlot::SetCurrentContext(ui->implot_context());
    ImPlot3D::SetCurrentContext(ui->implot3d_context());
    return true;
}
} // namespace ui

struct AppletMeta {
    const char* id;
    const char* version;
    const char* name;
    const char* summary;
    const char* tag;
    const char* services[15];   // NULL-terminated by aggregate zero-init
};

} // namespace caliper

// Generates: descriptor + the five C bridge functions + the single export.
// Field order is fixed: id, version, name, summary, tag, services.
#define CALIPER_APPLET(CLASS, ...)                                             \
    namespace caliper_applet_gen {                                             \
    static const ::caliper::AppletMeta kMeta{__VA_ARGS__};                     \
    struct Holder {                                                            \
        CLASS obj;                                                             \
        ::caliper::Host host;                                                  \
    };                                                                         \
    static void* cal_create(void) {                                            \
        try { return new Holder(); } catch (...) { return nullptr; }           \
    }                                                                          \
    static void cal_destroy(void* s) {                                         \
        try { delete static_cast<Holder*>(s); } catch (...) {}                 \
    }                                                                          \
    static bool cal_initialize(void* s, const CaliperHost* h) {                \
        auto* hold = static_cast<Holder*>(s);                                  \
        hold->host = ::caliper::Host(h);                                       \
        ::caliper::ui::connect(h);                                             \
        try { return hold->obj.on_init(hold->host); }                          \
        catch (...) {                                                          \
            hold->host.log_error("unhandled exception in on_init");            \
            return false;                                                      \
        }                                                                      \
    }                                                                          \
    static void cal_frame(void* s, const CaliperFrameInfo* fi) {               \
        auto* hold = static_cast<Holder*>(s);                                  \
        try { hold->obj.on_frame(::caliper::Frame::from(*fi)); }               \
        catch (...) {                                                          \
            hold->host.log_error("unhandled exception in on_frame");           \
        }                                                                      \
    }                                                                          \
    static void cal_cleanup(void* s) {                                         \
        auto* hold = static_cast<Holder*>(s);                                  \
        try { hold->obj.on_cleanup(); }                                        \
        catch (...) {                                                          \
            hold->host.log_error("unhandled exception in on_cleanup");         \
        }                                                                      \
    }                                                                          \
    } /* namespace caliper_applet_gen */                                       \
    extern "C" CALIPER_EXPORT const CaliperAppletDescriptor*                   \
    caliper_applet_descriptor(void) {                                          \
        static const CaliperAppletDescriptor kDesc = {                         \
            (uint32_t)sizeof(CaliperAppletDescriptor),                         \
            CALIPER_ABI_EPOCH,                                                 \
            ::caliper_applet_gen::kMeta.id,                                    \
            ::caliper_applet_gen::kMeta.version,                               \
            ::caliper_applet_gen::kMeta.name,                                  \
            ::caliper_applet_gen::kMeta.summary,                               \
            ::caliper_applet_gen::kMeta.tag,                                   \
            ::caliper_applet_gen::kMeta.services,                              \
            { (uint32_t)sizeof(CaliperAppletAPI),                              \
              &::caliper_applet_gen::cal_create,                               \
              &::caliper_applet_gen::cal_destroy,                              \
              &::caliper_applet_gen::cal_initialize,                           \
              &::caliper_applet_gen::cal_frame,                                \
              &::caliper_applet_gen::cal_cleanup } };                          \
        return &kDesc;                                                         \
    }