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:
.id— reverse-DNS identifier, must matchcaliper.toml.version— applet semver, must match the manifest.name— human-facing title.summary— one-line description.tag— category label.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 kMetaholding the six fields. - A
Holderstruct pairing your class instance with acaliper::Host. - Five exception-safe
extern "C"bridge functions —create,destroy,initialize,frame,cleanup— that own thetry/catchso no C++ exception ever crosses the C boundary (a throw is caught, logged viahost.log_error("unhandled exception in on_<phase>"), and swallowed;createreturnsnullptron a failednew). caliper::ui::connect(host)is called insideinitialize, beforeon_init, so ImGui/ImPlot contexts are live by the time your code runs.- The single exported symbol
caliper_applet_descriptor()returning astatic const CaliperAppletDescriptorwired tokMetaand 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:
- Fetches
caliper.ui.v1viaget_service. If the host does not vend it (headless),connectreturnsfalseand does nothing — this is why the fixture host and other headless drivers work without a GL context. - 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). - Calls
ImGui::SetCurrentContext,ImPlot::SetCurrentContext, andImPlot3D::SetCurrentContextwith 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; \
}