Skip to content

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:

  1. UIImGui::/ImPlot::/ImPlot3D:: calls inside your per-frame function, drawn into windows the host docks and composites.
  2. Compute — work you submit to jobs.v1, running on host-owned worker threads.
  3. 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 id and version must 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 .cpp generates the epoch-2 C ABI glue — the descriptor plus exception walls on every entry point. You never write extern "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:

examples/hello/hello.cpp
// 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})
examples/hello/hello.caliper.toml
[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"]
examples/hello/CMakeLists.txt
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

  1. This page — the lay of the land.
  2. Your first applet — walk hello line by line.
  3. Your first ML applet — the staged climb: compute, then torch, then live plots, then the bridge — one capability per stage.
  4. applets/embed_scope/ — the exemplar; every framework capability in one annotated file, structured to be copied.
  5. ML applet cookbook — the composition idioms (threading, cadences, the device-resident pull, viewport policy).
  6. Per-service reference pages — the contracts, when you need exact semantics.