Development basics¶
What building on Caliper actually looks like: what code you write, what code is already running, where every library comes from, and the edit-build-run loop. Read this before Your first applet; read the ML applet cookbook after it.
The mental model¶
When your applet runs, the host is already a living program: it owns the window, the renderer (Metal on macOS, Vulkan on Windows, GL fallback), the ImGui/ImPlot/ImPlot3D contexts, the job system, the metrics/artifacts/data stores, and the docked desktop. You write one shared library that the host loads at runtime. Your code contributes exactly three things:
- UI —
ImGui::/ImPlot::/ImPlot3D::calls inside your per-frame function, drawn into windows the host docks and composites. - Compute — work you submit to
jobs.v1, running on host-owned worker threads. - State — whatever your applet remembers between frames.
Everything else — event loop, GPU device, rendering, persistence, crash containment — is the host's job. You never initialize a window, a GL/Metal context, or an ImGui context; if you find yourself wanting to, you're fighting the model.
Where every library comes from¶
The repository and CMake build wire the project dependencies together. Platform
toolchains and system prerequisites must still be installed; see the root
README.md before configuring a build.
| Library | Comes from | You link it via | Notes |
|---|---|---|---|
| SDK headers (ABI, services, sugar, adapters) | sdk/include/ in this repo |
caliper::sdk |
Header-only INTERFACE target — nothing compiles into the SDK itself |
| ImGui (docking) + ImPlot + ImPlot3D + FileDialog | third_party/ submodules, pinned |
caliper::ui_stack |
Compiled once in-tree. Never bring your own copy — the pin is part of the ABI (§9 of PLATFORM.md): your applet and the host must agree on ImGui's memory layout byte-for-byte |
| libtorch | third_party/libtorch/ (vendored) |
"${TORCH_LIBRARIES}" + the rpath block |
The current root executable links the configured Torch libraries, and ML applets may link them as well. |
| curl, zlib | macOS system SDK | CURL::libcurl, ZLIB::ZLIB |
Only if you download/decompress data. Data acquisition is applet business — the host has no downloader |
| DuckDB | host-internal | you can't | Deliberately unreachable. You get its powers through services: metrics.v1, artifacts.v1, data.v1. No DuckDB type ever crosses the ABI |
The division is deliberate: what must be shared (UI stack) is pinned and provided; what is yours (torch, curl) you link privately; what is persistent (DuckDB) hides behind frozen service tables.
Anatomy of an applet¶
Three files, one folder. This is the entire footprint:
examples/hello/
├── hello.caliper.toml the manifest — checked BEFORE your code loads
├── hello.cpp your applet class + the CALIPER_APPLET macro
└── CMakeLists.txt ~12 lines (hello) to ~40 (with torch)
- Manifest: id (reverse-DNS), name, version, summary, required/optional
service lists. The
idandversionmust be byte-identical between the manifest and the descriptor in plugin.cpp, or the loader refuses (with a polite card, not a crash). See manifest reference. - The macro: the
CALIPER_APPLET(...)at the bottom of your.cppgenerates the epoch-2 C ABI glue — the descriptor plus exception walls on every entry point. You never writeextern "C"yourself. - The class: three lifecycle methods.
initialize(Host&)— probe your services (required ones you assert; optional ones degrade).draw_ui()— called every frame on the frame thread; submit ImGui windows, read worker-published state.cleanup()— cancel jobs, bounded-wait, release textures, return. - CMakeLists: copy hello's verbatim for a UI-only applet; copy the exemplar's for an ML applet. The only parts you edit: target name, source files, manifest filename.
The smallest complete applet, in full¶
This is everything — all three files. It is built in this repo as
examples/hello/ (the Hello card in your launcher), and the listings
below are the actual files, embedded verbatim — they cannot drift from what
compiles. Your first applet walks this same file line by
line:
// Epoch-2 fixture applet (PLATFORM.md §13.1): loader-test substrate and the
// "hello world" of the sugar layer. Kept deliberately tiny.
#include <caliper/caliper.hpp>
#include <cmath>
#include <cstdlib>
#include <vector>
class HelloApplet final : public caliper::Applet {
public:
bool on_init(caliper::Host& host) override {
host_ = &host;
crash_on_frame_ = std::getenv("CALIPER_HELLO_CRASH") != nullptr;
// Test hook: fail launch cleanly (initialize() returns false) so hosts
// can exercise the failed-load path without a crash. on_cleanup is NOT
// called for a false return (the loader destroys the raw instance), so
// this logs nothing.
if (std::getenv("CALIPER_HELLO_INIT_FAIL") != nullptr) return false;
host.log_info("hello.on_init");
return true;
}
void on_frame(const caliper::Frame& f) override {
if (crash_on_frame_) { // test hook: fault before any ImGui call
volatile int* p = nullptr;
*p = 1;
}
ImGui::SetNextWindowPos({40, 60}, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize({520, 360}, ImGuiCond_FirstUseEver);
ImGui::Begin("Hello, Caliper");
ImGui::Text("ABI epoch %d applet via CALIPER_APPLET macro", CALIPER_ABI_EPOCH);
ImGui::Text("framebuffer: %d x %d px dpi_scale: %.1f",
f.fb_width, f.fb_height, f.dpi_scale);
// Input: one button that owns the animation. Its label is an
// expression over the state it controls, so it reads "Pause" while
// running and "Play" while stopped; the click flips that state. A
// button returns true only on the frame it is clicked, and the state
// it toggles lives in the applet — not ImGui.
if (ImGui::Button(playing_ ? "Pause" : "Play")) playing_ = !playing_;
// The animation runs on phase the applet accumulates itself, advanced
// only while playing. f.time_sec (monotonic wall-clock) can't be
// paused — it keeps ticking whatever the applet does — so owning the
// phase is what makes Pause possible at all.
if (playing_) phase_ += (float)f.delta_sec;
if (ImPlot::BeginPlot("sine", {-1, 220})) {
static std::vector<float> xs(256), ys(256);
for (int i = 0; i < 256; i++) {
xs[i] = i / 255.0f * 6.28318f;
ys[i] = std::sin(xs[i] + phase_);
}
ImPlot::PlotLine("sin", xs.data(), ys.data(), 256);
ImPlot::EndPlot();
}
ImGui::End();
}
void on_cleanup() override {
if (host_) host_->log_info("hello.on_cleanup");
}
private:
caliper::Host* host_ = nullptr;
bool crash_on_frame_ = false;
float phase_ = 0.0f; // animation phase the applet owns (so Pause works)
bool playing_ = true; // Play/Pause state — starts running
};
CALIPER_APPLET(HelloApplet,
.id = "dev.caliper.hello",
.version = "0.1.0",
.name = "Hello",
.summary = "Epoch-2 fixture applet: sugar demo + loader-test substrate.",
.tag = "Demo",
.services = {CALIPER_UI_V1, CALIPER_LOG_V1})
[applet]
id = "dev.caliper.hello"
name = "Hello"
version = "0.1.0"
summary = "Epoch-2 fixture applet: sugar demo + loader-test substrate."
tag = "Demo"
[compat]
abi_epoch = 2
min_host = "0.6.0"
[services]
required = ["caliper.ui.v1", "caliper.log.v1"]
add_library(hello_applet SHARED hello.cpp)
target_link_libraries(hello_applet PRIVATE caliper::sdk caliper::ui_stack)
target_compile_definitions(hello_applet PRIVATE CALIPER_APPLET_EXPORT)
set_target_properties(hello_applet PROPERTIES
OUTPUT_NAME hello
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/applets"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/applets"
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON)
add_custom_command(TARGET hello_applet POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/hello.caliper.toml
${CMAKE_BINARY_DIR}/applets/hello.caliper.toml)
Note what is absent: no main, no window, no GL/Metal, no ImGui
context creation, no event loop, no extern "C". The macro plus the host
supply all of it. Growing from here toward ML means adding a jobs.v1
training job and the bridge — that path is the
cookbook, and its finished form is the
exemplar.
The development loop¶
# 1. create — the applets/* glob auto-discovers it (CONFIGURE_DEPENDS);
# no root CMake edits, just build:
mkdir applets/my_applet # + the three files (copy examples/hello/ to start)
# 2. build — your dylib + manifest land in build/applets/
cmake --build build --target my_applet -j
# 3. run — it appears as a card in the launcher
./build/caliper
# 4. iterate fast — skip the launcher click every rebuild:
CALIPER_AUTOLAUNCH=dev.example.my-applet ./build/caliper
# (CALIPER_EXIT_AFTER=<sec> exists too — clean-exit soak for CI)
--clean-first on a single applet target prunes its siblings
The applet list is a CONFIGURE_DEPENDS glob, so all applets are
configured but only the target you name gets built. A partial
cmake --build build --target my_applet --clean-first cleans the whole
build tree first, then rebuilds only my_applet — leaving the other
applets' dylibs and manifests missing from build/applets/ until a full
cmake --build build restores them. If the launcher suddenly shows only
one card, this is why: rebuild without --clean-first, or build the
default target to bring every applet back.
In CLion the same works via the CMake tool window — reload the project once
after creating the folder, then the my_applet target exists.
Testing without a window: the fixture host
(caliper::sdk_testing, <caliper/fixture_host.h>) fakes get_service
with tables you inject — see tests/test_sugar_services.cpp for the
pattern. Applet logic that reads services can be unit-tested headlessly.
Your data lives at ~/Library/Application Support/Caliper/data/<your-id>/
— the host creates it and hands it to you as host.data_dir(). Downloads,
caches, scratch files go there and nowhere else.
The rules (each one guards something real)¶
- No raw GL/Metal/Vulkan calls in applets (§6c). Draw through
ImGui/ImPlot/ImPlot3D; get tensors on screen through
tensor_bridge.v1. This is what makes your applet run identically on both backends. - Bridge and data.v1 calls: frame thread only. Torch ops: worker thread only. The cookbook §1 has the full spine.
- Honor cancel within 100 ms in every job loop (check per batch, per download chunk).
- Don't catch your way across the ABI — the sugar's exception walls already guarantee nothing throws into the host; inside your own code, handle errors normally.
- If a service is optional, degrade visibly — a disabled panel saying "metrics: absent (ok)" beats silent absence. Every optional feature of the exemplar shows the idiom.
Reading order¶
- This page — the lay of the land.
- Your first applet — walk hello line by line.
- Your first ML applet — the staged climb: compute, then torch, then live plots, then the bridge — one capability per stage.
applets/embed_scope/— the exemplar; every framework capability in one annotated file, structured to be copied.- ML applet cookbook — the composition idioms (threading, cadences, the device-resident pull, viewport policy).
- Per-service reference pages — the contracts, when you need exact semantics.