Your first applet¶
(New here? Development basics explains what you write vs. what the host provides, and where every library comes from.)
This walks the hello applet — the smallest complete Caliper applet on ABI
epoch 2. It is the canonical starting point: one manifest, one macro, three
lifecycle methods. Continue to examples/sine_scope/ for a small ML-oriented
example. The in-tree applets demonstrate larger combinations of services; no
single exemplar should be treated as covering the entire service catalog.
The whole applet lives in examples/hello/:
examples/hello/
├── hello.caliper.toml # the manifest — identity + what the host must provide
├── hello.cpp # the applet — one class + one macro
└── CMakeLists.txt # the build — links caliper::sdk + caliper::ui_stack
1. The manifest¶
The host reads hello.caliper.toml before it loads any of your code. It
declares who the applet is, which ABI epoch it was compiled against, and which
framework services it needs to run:
[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"]
required services are a gate: if the host cannot provide caliper.ui.v1 or
caliper.log.v1, it refuses to load the applet rather than handing you a
half-working Host. The id, version, and services here must agree with
the fields you pass to the CALIPER_APPLET macro (below) — the loader verifies
the two and rejects the applet if they drift. See
reference/manifest.md for the full schema.
2. The macro¶
One include and one class is the entire C++ surface. #include
<caliper/caliper.hpp> pulls in the sugar layer (the Applet/Host/Frame
types) and the pinned ImGui/ImPlot stack — there is no wrapper to learn, you
program raw ImGui:
// 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})
The CALIPER_APPLET(HelloApplet, ...) macro at the bottom is the whole ABI
boundary of the dylib. It generates the descriptor the loader looks for
(caliper_applet_descriptor), the five exception-safe C bridge functions, and
the ui::connect() call that shares the host's ImGui/ImPlot contexts and
allocators with your dylib — so ImGui:: and ImPlot:: calls land in the host's
single UI world. Field order is fixed: id, version, name, summary,
tag, services.
3. The lifecycle: on_init → on_frame → on_cleanup¶
Your class overrides three methods from caliper::Applet:
-
on_init(Host& host)runs once when the applet is opened. TheHost&is valid for your whole lifetime — keep the pointer. Do setup here, and log through the host (host.log_info(...)), neverprintf(see howto/debug-an-applet.md for why). Returnfalseto abort loading. -
on_frame(const Frame& f)runs every frame. Everything visible happens here, and nothing slow — you share the frame thread with the host and every other applet.f.fb_width/f.fb_heightare physical pixels;f.dpi_scaleconverts to the logical units ImGui sizes in; drive animation fromf.time_sec/f.delta_sec, never a wall-clock sleep (§3a shows why a pausable animation accumulates its own phase fromf.delta_secrather than readingf.time_secdirectly). -
on_cleanup()runs when the applet closes — symmetric withon_init: persist, release, log. After it returns the host destroys your object; do not touch host services afterwards.
Hello also reads CALIPER_HELLO_CRASH in on_init and, when set, faults inside
on_frame before any ImGui call. That is a deliberate test hook the loader's
crash-quarantine tests use — not something your own applets need.
3a. Input: the Play/Pause button¶
The button above the plot is the smallest complete lesson in ImGui IO, and it turns on three ideas you will use in every applet:
-
A widget call both draws and reports.
ImGui::Button(...)draws the button and returnstrueon the single frame it was clicked. There is no callback and no event queue — you check the return value inline:This is immediate mode: the UI is a function of your state, re-issued every frame, and input comes back as the return value of the call that drew it.if (ImGui::Button(playing_ ? "Pause" : "Play")) playing_ = !playing_; -
The label is derived from state, every frame. There is one button, not a Play button and a Pause button — its label is an expression over
playing_, recomputed on everyon_frame. Because the whole UI is rebuilt each frame from your state, a widget that reflects state costs nothing extra: you just compute what to show. The click flips the sameboolthe label reads, so the button relabels itself the very next frame. -
The state lives in your applet, not in ImGui. ImGui does not remember "paused" for you —
playing_is a member ofHelloApplet. The widget reads and writes your field; ImGui only owns pixels and the click. That is why the state is aboolon the class, initialised in the header, not astaticinsideon_frame.
The subtle part is why the animation can be paused at all. Earlier the sine
was drawn from f.time_sec — the host's monotonic wall-clock, which keeps
advancing no matter what the applet does, so there is nothing you could freeze.
Pause only becomes possible once the applet owns the time: Hello accumulates
its own phase_, advanced by f.delta_sec only while playing:
if (playing_) phase_ += (float)f.delta_sec; // ...then draw sin(x + phase_)
That is the general shape of interactive animation in a Caliper applet — derive
what you draw from state you control, and let the widgets edit that state. The
same three ideas scale straight up to sliders (SliderFloat returns an edited
value), checkboxes, and the live training controls the ML applets use.
4. Build it¶
The CMakeLists.txt links the two SDK targets and drops the dylib plus a copy
of the manifest into build/applets/, where the host scans for applets:
cmake -B build
cmake --build build --target hello_applet
ls build/applets/ # libhello.dylib + hello.caliper.toml
In-tree the build links caliper::sdk and caliper::ui_stack directly; an
out-of-tree applet swaps those two lines for a find_package/CPM fetch of a
tagged SDK release under the same target names — nothing else changes.
5. See it in the app¶
Hello appears on the launcher landing page as a card you can open. The host's
epoch-2 loader discovers it by reading its <stem>.caliper.toml manifest and the
caliper_applet_descriptor export — the two signals every applet you build this
way must ship. If the card is missing, check that both are present (the dylib and
its manifest sit side by side in build/applets/); if the card shows an
[unavailable] line instead, that line is the loader's refusal reason (see the
refusal reference).