Skip to content

Embedding Caliper (libcaliper)

libcaliper is the framework core — applet loader, service registry, the host-neutral services, HostRenderer (Metal/Vulkan), the tensor bridge, and the geometry ladder — behind a small C ABI so a host binary can run the applet canvas inside a view it owns without linking ImGui, torch, or any renderer type. The caliper executable is its first embedder; the second in-tree embedder is examples/embed_host/ — a ~254-line AppKit host that this page mirrors.

This is the embedder-facing contract (hosts). It is NOT the applet-facing ABI: applets link caliper::sdk and use caliper/abi.h + the C++ sugar. caliper/embed.h lives on a separate include root so an applet physically cannot #include it — embedding a core from inside an applet is a category error the include topology forbids.

Status (v0)

L1+L2 shipped 2026-07-11 (branch feat/libcaliper), run-proven on both ecosystems — Metal on Apple Silicon and Vulkan/HWND on Windows (RTX 500 Ada, 2026-07-11). Design + phase outcomes: docs/superpowers/specs/2026-07-11-libcaliper-compass-design.md.

The five calls

An embedder drives the core with exactly five calls, pumping frames from its own event loop (the core never owns the process loop — that is the difference between a library and a host):

  1. caliper_core_create — spin up the core (renderer + services + loader).
  2. caliper_core_attach_canvas — hand it the native view (NSView* / HWND) it should paint, or an offscreen target.
  3. caliper_core_load_applet — launch one applet by manifest id (e.g. dev.caliper.instance-scope). A canvas must be attached first — an applet's first frame touches the renderer's ImGui backend, so loading before attach is an honest refusal, not a crash.
  4. caliper_core_frame / caliper_core_event — pump exactly ONE frame from your loop (wx idle/timer, a CVDisplayLink, a Win32 message loop), and translate your toolkit's input into CaliperInputEvent and forward it.
  5. caliper_core_shutdown — tear it all down (the exact reverse of create) on window close; clears the one-core-per-process lock.
CaliperCoreDesc desc = { .struct_size = sizeof desc };
desc.renderer = CALIPER_RENDERER_DEFAULT;      /* Metal on Apple, Vulkan on Windows */
CaliperCore* core = caliper_core_create(&desc);

CaliperCanvasDesc canvas = { .struct_size = sizeof canvas };
canvas.mode = CALIPER_CANVAS_WINDOW;           /* native_view is an NSView* / HWND */
canvas.width = w; canvas.height = h; canvas.content_scale = scale;
caliper_core_attach_canvas(core, native_view, &canvas);

caliper_core_load_applet(core, "dev.caliper.instance-scope");

/* ... from YOUR event loop, once per frame: */
caliper_core_frame(core);
/* ... on each input, translated to a toolkit-neutral event: */
caliper_core_event(core, &event);

caliper_core_shutdown(core);

Ownership (design §4)

The load-bearing decision: libcaliper owns the applet canvas end-to-end. The embedder supplies a native child view; the core runs the ImGui context, HostRenderer, bridge, and geometry inside it. A host's own chrome (Compass's wx AUI docking, property grids, menus) wraps around those canvases and never paints applet pixels.

Concern Owner Note
Process event loop Embedder caliper_core_frame does one frame and returns — no polling, no vsync wait
Native window / view handle Embedder NSView* / HWND passed to attach_canvas
Input events Embedder translates → core consumes your GLFW/AppKit/wx event becomes a CaliperInputEvent; no toolkit types cross
ImGui context (one per canvas) libcaliper the embedder never touches ImGui state; allocator handoff stays internal
HostRenderer + tensor bridge + geometry libcaliper the zero-copy claim travels with the core
Applet loader + service registry + services libcaliper the same registry applets already see
libtorch (one per process, D5) libcaliper the embedder must NOT link its own torch; the core owns device/pack policy
Crash containment libcaliper applet faults are caught by the core's guard and surfaced via crash_fn; the embedder is not taken down

Honest caveats (v0)

  • One CaliperCore per process. caliper_core_create refuses a second live core with a NULL return (the one-libtorch-per-process policy, D5, already binds the process); shut the first down first.
  • CaliperCoreDesc.data_dir is IGNORED. The process app-data path is always used; threading a per-core data root is a host_services signature change deferred past R4.
  • The applet caliper.log.v1 service bypasses log_fn and writes to process stderr in v0. Core diagnostics (renderer pick, refusals, crash text) DO route through log_fn; applet log lines do not.
  • CALIPER_CANVAS_WINDOW has no ctest coverage. The windowed canvas is run-proven live on both OSes — Metal (Apple Silicon) and Vulkan/HWND (Windows, RTX 500 Ada) — but there is no automated coverage of window mode on either; the live embed_host run stays the ritual. Automated byte-exactness rides the OFFSCREEN read_pixels battery (the §7 host-axis byte-compare), green on both backends.
  • GL is not an embed target. GL's context ownership is GLFW-coupled chrome (D13, the frozen fallback); a core whose resolved backend is GL refuses at attach_canvas ("embed requires Metal or Vulkan").

The header, verbatim

This page embeds the real header; the docs build fails if the file moves.

/* ===========================================================================
 * caliper/embed.h — the embed C ABI (libcaliper / Compass R4, L2a)
 *
 * This is the SEAM that makes "embeddable" true: the small C ABI a host binary
 * (Compass's wx chrome, examples/embed_host, the caliper exe eventually) uses
 * to run the applet canvas — ImGui + HostRenderer + bridge + geometry — inside
 * a view it owns, WITHOUT linking ImGui, torch, or any renderer type.
 *
 * WHO CALLS THIS: embedders (hosts). NOT applets. The applet-facing ABI is
 * <caliper/abi.h> + the sugar; this header lives on a SEPARATE include root
 * (top-level include/, a PUBLIC include dir of the libcaliper target only) so
 * an applet — which links caliper::sdk (sdk/include) and never libcaliper —
 * physically cannot #include it. An applet embedding a core would be a category
 * error; the include topology forbids it.
 *
 * C, not C++ (mirrors D1, the applet contract): a host built years apart from
 * libcaliper must still embed it. C++ sugar for host authors can ship later.
 *
 * ---------------------------------------------------------------------------
 * LIFECYCLE & THREADING CONSTRAINTS (design §4.3 — verbatim, an embedder that
 * ignores these gets crashes the core cannot prevent):
 *
 *  - THE CORE NEVER OWNS THE EVENT LOOP. caliper_core_frame() does exactly ONE
 *    frame and returns: no polling, no sleeping, no vsync wait. The embedder
 *    calls it from ITS loop (wx idle/timer, a GLFW loop, a CVDisplayLink, ...).
 *    That is the whole difference between a library and a host.
 *
 *  - INPUT CROSSES AS DATA, NOT TOOLKIT TYPES. The embedder translates its own
 *    GLFW/AppKit/wx events into CaliperInputEvent; the core feeds ImGuiIO. No
 *    GLFWwindow, NSEvent, or wxEvent ever appears here.
 *
 *  - ONE ImGui CONTEXT PER CANVAS, owned by the core. The embedder never
 *    touches ImGui state; the allocator handoff stays internal.
 *
 *  - ONE CaliperCore PER PROCESS in v0 (the one-libtorch-per-process policy,
 *    D5, already binds the process). caliper_core_create refuses a second live
 *    core with a NULL return; shut the first down first.
 *
 *  - CRASH CONTAINMENT. An applet fault is caught by the core's existing crash
 *    guard, surfaced through CaliperCoreDesc.crash_fn, and the applet is
 *    quarantined — the embedder is NOT taken down with it.
 *
 *  - FRAME-THREAD DISCIPLINE carries over: call frame()/event()/attach/read
 *    from ONE thread (the UI thread). Applet torch work runs on jobs threads
 *    and draws from snapshots; that contract is unchanged and internal.
 * ===========================================================================*/
#ifndef CALIPER_EMBED_H
#define CALIPER_EMBED_H

#include <stddef.h>   /* size_t   */
#include <stdint.h>   /* uint32_t */

#ifdef __cplusplus
extern "C" {
#endif

/* Bumped when this ABI grows a field. The struct_size FIRST-member on every
 * struct is the compatibility gate, and the rule is exact: the core requires
 * caller->struct_size >= the core's own sizeof(...) and REFUSES otherwise (a
 * caller built against an OLDER, smaller header is rejected, not silently
 * misread). Fields are append-only; when the ABI grows, CALIPER_EMBED_API_VERSION
 * bumps and the new fields land after the old ones, so a caller compiled against
 * a header at least as new as the core passes the size gate and every field the
 * core reads is present. Newer-caller / older-core is out of scope in v0 (one
 * libcaliper per process, built together). */
#define CALIPER_EMBED_API_VERSION 1

/* ---------------------------------------------------------------------------
 * v1.1 — the "consumer" pass (docs/superpowers/specs/2026-07-12-compass-
 * consumer-design.md §3). ALL additive; every v1 struct is byte-stable and no
 * struct grew a member, so CALIPER_EMBED_API_VERSION stays 1 (the size gate is
 * about struct fields; v1.1's additions are a new free function plus semantics
 * activated on EXISTING reserved fields). A v1 caller links and runs unchanged.
 *
 * HONESTY REGISTER (what v1.1 changed against the v0 gaps):
 *   - get_service           ADDED  — caliper_core_get_service: a host consumes
 *                                     the applets' service tables (P2/P3).
 *   - data_dir gap          CLOSED — CaliperCoreDesc.data_dir now roots the
 *                                     stores (was reserved/ignored in v0).
 *   - log-singleton gap     CLOSED — applet caliper.log.v1 lines route to
 *                                     CaliperLogFn (was stderr-only in v0).
 *   - one-core-per-process  REMAINS — create still refuses a second live core.
 *   - one-canvas-per-core   REMAINS — attach_canvas still refuses a second.
 * ------------------------------------------------------------------------- */

/* Opaque handle. One live instance per process in v0 (see header note). */
typedef struct CaliperCore CaliperCore;

/* Which HostRenderer backend the core embeds. GL is intentionally ABSENT: its
 * context ownership is GLFW-coupled chrome (D13, the frozen fallback), never an
 * embed target. DEFAULT resolves to the platform backend (Metal on Apple,
 * Vulkan on Windows) honoring CALIPER_RENDERER. A core whose resolved backend
 * cannot embed refuses at attach_canvas ("embed requires Metal or Vulkan"). */
typedef enum CaliperRenderer {
    CALIPER_RENDERER_DEFAULT = 0,
    CALIPER_RENDERER_METAL   = 1,
    CALIPER_RENDERER_VULKAN  = 2
} CaliperRenderer;

/* Diagnostics sink. NULL -> stderr. Receives BOTH streams, distinguishable by a
 * prefix on the message (v1.1):
 *   - CORE diagnostics (renderer pick, refusals, crash text) arrive UNtagged.
 *   - APPLET caliper.log.v1 lines arrive tagged "[applet] " (v1.1 routes the
 *     applet log service here when this callback is set — it was stderr-only in
 *     v0; a host surfaces them in a native Log pane, not stderr).
 * `level` is the CaliperLogLevel value (0=DEBUG..3=ERROR). `message` is valid
 * only for the call. Called from the frame thread for core lines, and from any
 * thread for applet log lines (log.v1 is worker-callable) — a host sink that
 * touches shared UI state must marshal to its UI thread itself. */
typedef void (*CaliperLogFn)(void* userdata, int level, const char* message);

/* Applet-fault callback (§4.3). Fired AFTER the faulting applet is quarantined
 * and torn down; the core keeps running. applet_id/fault are valid only for the
 * duration of the call. NULL -> the fault is logged and swallowed. */
typedef void (*CaliperCrashFn)(void* userdata, const char* applet_id,
                               const char* fault);

typedef struct CaliperCoreDesc {
    size_t          struct_size;   /* = sizeof(CaliperCoreDesc); FIRST member. */
    CaliperRenderer renderer;      /* backend to embed.                        */
    const char*     data_dir;      /* v1.1: per-core app-data root. NON-NULL
                                    * routes metrics/artifacts/applet-data under
                                    * this directory (created if missing) — a
                                    * document app's per-project root. NULL keeps
                                    * the OS default byte-for-byte (the caliper
                                    * exe passes NULL). Valid for one live core;
                                    * restored on shutdown (one core per process).*/
    const char*     applets_dir;   /* extra applet scan dir; NULL -> default
                                    * discovery (app-data/applets + exe-side). */
    CaliperLogFn    log_fn;        /* NULL -> stderr.                          */
    CaliperCrashFn  crash_fn;      /* NULL -> log-and-swallow.                 */
    void*           userdata;      /* passed back to log_fn / crash_fn.        */
} CaliperCoreDesc;

typedef enum CaliperCanvasMode {
    CALIPER_CANVAS_WINDOW    = 0,  /* native_view is an NSView* / HWND.        */
    CALIPER_CANVAS_OFFSCREEN = 1   /* no view; render to a texture, read back. */
} CaliperCanvasMode;

typedef struct CaliperCanvasDesc {
    size_t            struct_size; /* = sizeof(CaliperCanvasDesc); FIRST.      */
    CaliperCanvasMode mode;
    int               width;       /* physical pixels.                        */
    int               height;      /* physical pixels.                        */
    float             content_scale; /* DPI scale (1.0 = 1x); <=0 -> 1.0.     */
} CaliperCanvasDesc;

typedef enum CaliperEventType {
    CALIPER_EVENT_MOUSE_MOVE    = 0, /* uses x, y (physical px)               */
    CALIPER_EVENT_MOUSE_BUTTON  = 1, /* uses button, down                     */
    CALIPER_EVENT_MOUSE_SCROLL  = 2, /* uses dx, dy                           */
    CALIPER_EVENT_KEY           = 3, /* uses key (== ImGuiKey), down, mods    */
    CALIPER_EVENT_TEXT          = 4, /* uses codepoint                        */
    CALIPER_EVENT_RESIZE        = 5, /* uses width, height (physical px)      */
    CALIPER_EVENT_CONTENT_SCALE = 6, /* uses scale                            */
    CALIPER_EVENT_FOCUS         = 7  /* uses focused (0/1)                    */
} CaliperEventType;

/* Bit flags for CaliperInputEvent.mods (a KEY event's modifier state). */
enum {
    CALIPER_MOD_CTRL  = 1 << 0,
    CALIPER_MOD_SHIFT = 1 << 1,
    CALIPER_MOD_ALT   = 1 << 2,
    CALIPER_MOD_SUPER = 1 << 3
};

/* One toolkit-neutral input event. Only the fields named in the CaliperEventType
 * comment above are read for a given type; leave the rest zero. */
typedef struct CaliperInputEvent {
    size_t           struct_size;  /* = sizeof(CaliperInputEvent); FIRST.     */
    CaliperEventType type;
    float            x, y;         /* mouse position (physical px)            */
    float            dx, dy;       /* scroll delta                            */
    int              button;       /* 0=left, 1=right, 2=middle               */
    int              down;         /* 0/1 for button/key press state          */
    int              key;          /* CaliperKey == ImGuiKey value            */
    int              mods;         /* CALIPER_MOD_* bitset                     */
    unsigned int     codepoint;    /* UTF-32 for CALIPER_EVENT_TEXT           */
    int              width, height;/* CALIPER_EVENT_RESIZE (physical px)      */
    float            scale;        /* CALIPER_EVENT_CONTENT_SCALE             */
    int              focused;      /* CALIPER_EVENT_FOCUS                     */
} CaliperInputEvent;

/* --- Lifecycle ---------------------------------------------------------- */

/* Create the core (ImGui context + renderer + service registry + loader, in
 * the L1-proven order). Returns NULL and logs on: a second live core, an
 * unsupported renderer for this OS, or renderer init failure. */
CaliperCore* caliper_core_create(const CaliperCoreDesc* desc);

/* Tear down: unload the applet, join jobs, close stores, drop the renderer and
 * ImGui context — the exact reverse order of create (crash-order load-bearing,
 * see the impl). Safe on NULL. Clears the one-core-per-process lock. */
void caliper_core_shutdown(CaliperCore* core);

/* Attach the applet canvas. native_view is an NSView* / HWND for CANVAS_WINDOW,
 * ignored (pass NULL) for CANVAS_OFFSCREEN. Returns 1 on success, 0 on refusal
 * (backend can't embed, canvas already attached, bad size) — a 0 leaves the
 * core usable and sets last_error. v0: one canvas per core. */
int  caliper_core_attach_canvas(CaliperCore* core, void* native_view,
                                const CaliperCanvasDesc* desc);

/* Pump exactly ONE frame: clear the canvas, run the loaded applet's draw under
 * the crash guard, composite + present/store. No-op (sets last_error) if no
 * canvas is attached. Never blocks on the event loop. */
void caliper_core_frame(CaliperCore* core);

/* Feed one input event into the core's ImGuiIO. No-op before a canvas exists. */
void caliper_core_event(CaliperCore* core, const CaliperInputEvent* event);

/* --- Applet control (reuses the loader's manifest discovery) ------------- */

/* Load + launch the applet whose manifest id matches (e.g. "dev.caliper.hello").
 * Returns 1 on success, 0 if unknown/refused/failed (last_error set). A canvas
 * must be attached FIRST — an applet's launch/first frame touches the renderer's
 * ImGui backend, so load before attach_canvas is an honest refusal, not a crash.
 *
 * TEARDOWN-FIRST SEMANTICS. Any currently-loaded applet is torn down FIRST
 * (workers joined, then the instance), and only THEN is the new one launched —
 * so init jobs the new applet schedules are never cancelled by the old one's
 * teardown. Two consequences follow, both intentional:
 *   - Reloading the SAME id is a clean RESTART: the running instance is torn
 *     down (on_cleanup) and a fresh one launched (on_init) — no state carries
 *     over.
 *   - A FAILED launch leaves NO applet loaded, not the previous one: once the
 *     old applet is torn down it is gone even if the new launch is refused.
 * An UNKNOWN id is the one exception — it refuses up front (0, last_error set)
 * WITHOUT disturbing a running applet, so a typo cannot kill the live session. */
int  caliper_core_load_applet(CaliperCore* core, const char* manifest_id);

/* Tear down the loaded applet (jobs joined first, then instance). No-op if none
 * is loaded. Call between frames. */
void caliper_core_unload_applet(CaliperCore* core);

/* --- Offscreen readback (the automatable + byte-compare surface, §7) ----- */

/* Copy the LAST composited frame's pixels to buf as tightly-packed RGBA8,
 * `stride` bytes per row (>= width*4). Returns 1 on success, 0 if the canvas is
 * not offscreen, buf is NULL, or stride is too small. */
int  caliper_core_read_pixels(CaliperCore* core, void* buf, int stride);

/* Human-readable reason for the most recent refusal (empty string if none).
 * Valid until the next core call. Never NULL for a non-NULL core. */
const char* caliper_core_last_error(CaliperCore* core);

/* --- Service consumption (v1.1) — the host becomes a CONSUMER, not just a
 * picture-in-picture embedder ------------------------------------------------
 *
 * Returns the SAME service table an applet receives via CaliperHost.get_service
 * for `id` (the applets' own vocabulary: "caliper.metrics.v1", "caliper.jobs.v1",
 * "caliper.artifacts.v1", "caliper.data.v1", ...). Cast the result to the matching
 * Caliper<Name>V1 struct from the caliper/services headers and call its thunks
 * as an applet would — no renderer/torch/ImGui type crosses the seam (D3: the
 * interchange is C ABI + CaliperTensor + Arrow C streams). C++ hosts may use the
 * caliper.hpp sugar.
 *
 * Returns NULL for an unknown id or a NULL core. The pointer is a process-static
 * table (embed.h's pointer-validity guarantee): valid from create until
 * caliper_core_shutdown, after which — like every other call here — the
 * CaliperCore* is dead and must not be used.
 *
 * THREADING CONTRACT (§3.2 — VERIFIED against the implementations, not assumed;
 * an embedder that ignores it gets data races the core cannot prevent). Two
 * classes:
 *
 *  - ANY-THREAD services — call from any thread, including a host UI thread that
 *    is NOT the caliper_core_frame() thread, concurrently with an applet's
 *    worker writes:
 *      * caliper.metrics.v1   — MetricsStore holds ONE DuckDB connection under
 *                               ONE mutex; every writer AND reader (runs/
 *                               scalars/histograms) takes it, so host-thread
 *                               reads racing applet-thread writes serialize
 *                               (verified: metrics_store.cpp — lock_guard on
 *                               every method).
 *      * caliper.artifacts.v1 — same one-connection-one-mutex model; put/
 *                               path_of/exists/by_run all lock (artifact_store
 *                               .cpp). path_of returns a thread_local buffer,
 *                               valid until the next artifacts call ON THAT
 *                               THREAD.
 *      * caliper.data.v1      — one mutex over query/register/open_dataset
 *                               (data_store.cpp); last_error() is thread-local
 *                               (each thread sees ITS last failing call).
 *      * caliper.jobs.v1      — cross-thread BY DESIGN (submit/cancel/is_running
 *                               /progress over the process JobSystem).
 *      * caliper.device.v1    — reads an immutable negotiated-at-startup record.
 *      * caliper.log.v1       — reentrant; callable from worker threads (routes
 *                               to log_fn when installed, else stderr — below).
 *
 *  - FRAME-THREAD-ONLY services — call ONLY from the thread that calls
 *    caliper_core_frame(); they touch the renderer / the single ImGui context:
 *      * caliper.tensor_bridge.v1 / v1.1 / v1.2  (GPU upload, draw-adjacent)
 *      * caliper.geometry.v1 ... v1.3            (GPU draw)
 *      * caliper.ui.v1                           (the ImGui/ImPlot contexts —
 *                                                 meaningless to a host anyway)
 *
 * P3 caveat (D5, one torch per process, never the host's): a host pushes
 * PARAMETERS; an APPLET's worker produces device tensors. The bridge's host-side
 * use is CPU-staged uploads on the frame thread only. */
const void* caliper_core_get_service(CaliperCore* core, const char* id);

#ifdef __cplusplus
}  /* extern "C" */
#endif
#endif /* CALIPER_EMBED_H */