Writing capsules
A capsule is a static sig_capsule describing a name, its
terminals and optional lifecycle hooks. The two built-ins,
capsules/simulator/ (a source) and capsules/counter/ (keeps state), are the best
worked examples; this page explains the pieces.
The capsule
Section titled “The capsule”#include <sig/runtime/sig_capsule.h>
static sig_status_t on_value(sig_capsule_instance *inst, const sig_value *signal, const char *terminal_name) { /* runs on the owning service's worker thread */ return SIG_OK;}
static const sig_terminal my_inputs[] = { { SIG_TERMINAL_DEFAULT, on_value } };static const sig_terminal my_outputs[] = { { SIG_TERMINAL_DEFAULT, NULL } };
const sig_capsule my_capsule = { "mything", /* the "type" used in capsule .cfg files */ 1, my_inputs, 1, my_outputs, my_start, /* nullable */ my_stop, /* nullable */ sizeof(my_properties) / sizeof(my_properties[0]), my_properties, /* see "Properties" */ .category = "flow", /* nullable; see below */};nameis what a capsule file’s"type"refers to.- Input terminals need a
handler; output terminals set it toNULL. categorysays what the capsule is for and is shown by the management API (GET /capsules):"source"(only emits),"sink"(only consumes),"flow"(routes or filters a stream) or"state"(keeps state across signals).NULLleaves it uncategorized. Keep it last in the initializer, and use a designated initializer.- Use
SIG_TERMINAL_DEFAULT(__default_terminal_value) for the single conventional terminal, or your own names for more.
Hooks and threads
Section titled “Hooks and threads”| Code | Runs on |
|---|---|
start hook |
the thread that starts the service, before the worker thread exists |
| terminal handlers, scheduled jobs | the service’s worker thread |
stop hook |
the thread that stops the service, after the worker has been joined |
Because the worker is the only thread that runs handlers and jobs, an
instance’s storage and state need no locking as long as you only touch
them from those places (and from start/stop, which never overlap the
worker). To affect an instance from any other thread, send it a signal; do not
touch its state directly. Do not call sig_runtime_service_* from a handler,
job or hook (it can deadlock; see architecture.md).
Properties
Section titled “Properties”A capsule declares the properties it understands once, in a table. The
framework checks a configuration against the table, applies the defaults, and
hands your code typed values; you never parse a sig_value or repeat a default
yourself. The same table is published by GET /capsules so a UI can build a
form from it.
enum { MY_END, MY_STEP, MY_MODE }; /* the index of each property */
static const sig_property my_properties[] = { [MY_END] = SIG_PROP_INT_REQUIRED("end", "Inclusive upper bound"), [MY_STEP] = SIG_PROP_INT_MIN("step", 1, 1, "Increment"), /* default 1, at least 1 */ [MY_MODE] = SIG_PROP_STRING_CHOICES("mode", "fast", "fast|slow", "Speed"),};- Each entry has a name, a type (int, bool, double, string), either
requiredor a default, an optional int range, optional stringchoices(matched case-insensitively), and a one-line description. TheSIG_PROP_*macros insig_capsule.hbuild the entries; the designated initializers tie each entry to its enum value at compile time. - Read a value by index with
sig_capsule_instance_int/bool/double/string(inst, MY_END). They are safe instart, terminal handlers and jobs. A wrong index or type logs an error and returns zero rather than crashing. - When a service starts, each instance is resolved first
(
sig_capsule_instance_resolve): every value is checked (type, range, choices), required ones must be present, and defaults fill the rest. A problem fails the service’s start with the property named in the log. An integer is accepted for a double property; a double for an int property is an error. An unknown property is only a warning at start, butPUT/PATCHof a configuration through the API rejects it, so typos are caught when saving. - Unit tests that call a hook directly must call
sig_capsule_instance_resolveon the instance first. - A capsule with no table (
num_propertiesof 0) accepts any properties and reads them withsig_capsule_configuration_get_property(inst->configuration, key, &v); prefer a table.
- Storage is per-instance mutable state of
sig_valuetype:sig_capsule_instance_set_storage/get_storage.setshares the value (you still own and mustsig_value_dtoryours);getreturns a borrowed pointer that is invalidated by the nextseton the same key. - State is for anything that is not a
sig_value(a socket, a buffer):sig_capsule_instance_set_state(inst, ptr, dtor); the destructor runs when the instance is destroyed or the state is replaced.
Return a non-SIG_OK status from start to refuse to start; the service start then
fails (instances that had already started are stopped again, and the status is
returned to the caller). Missing or invalid properties are caught before start
runs, so a hook only needs to fail for problems the table cannot express.
Emitting and receiving signals
Section titled “Emitting and receiving signals”sig_value v = sig_value_int(42);sig_capsule_instance_notify(inst, SIG_TERMINAL_DEFAULT, &v);sig_value_dtor(&v);notify copies the value to each destination’s queue and returns without
waiting; with no connected receivers it is a no-op. In a handler, the signal
you receive is yours only for the duration of the call; copy it
(sig_value_copy/sig_value_dup) if you need to keep it.
Scheduling work
Section titled “Scheduling work”struct timespec interval = { .tv_sec = 0, .tv_nsec = 250 * 1000000L };sig_capsule_instance_schedule(inst, interval, /*repeat=*/true, my_job, NULL, NULL);my_job(inst, job_data) runs on the service’s worker thread. Intervals must be
at least 1 ms. Pass a sig_job ** as the last argument to keep a handle for
sig_capsule_instance_cancel. See the contract in sig_service.h
(missed-fire coalescing, retained one-shot nodes).
Registering it with the host
Section titled “Registering it with the host”- Create
capsules/<name>/<name>_capsule.hand.c, exportingextern const sig_capsule <name>_capsule;. Every capsule gets its own folder. - Add
<name>to theSIG_CAPSULESCMake list (defaultcounter;simulator;logger;filter;switch;state_change), e.g.-DSIG_CAPSULES="counter;simulator;mything". - Add an
#ifdef SIG_CAPSULE_<NAME>block tocapsules/sig_builtin_capsules.cthat includes your header and registers the capsule.
Optional sources and libraries: capsule.cmake
Section titled “Optional sources and libraries: capsule.cmake”If capsules/<name>/capsule.cmake exists, capsules/CMakeLists.txt includes it
while configuring that capsule. It may set:
SIG_CAP_REQUIRES: optional features the capsule needs.http_clientmakescapsules/CMakeLists.txtskip the capsule, with a warning, unlessSIG_HTTP_CLIENTis on.SIG_CAP_EXTRA_SOURCES: extra.cfiles compiled into the capsule’s library. Paths are relative to the capsule’s directory, or absolute (for example a shared platform source under${CMAKE_SOURCE_DIR}/src/).SIG_CAP_LIBS: libraries the capsule links (and its unit test), such assig::http_client.
The file can also call find_package (and message(FATAL_ERROR ...)) so only
builds that select the capsule need the dependency. The weather capsule
requires http_client and links sig::http_client, which brings libcurl
(capsules/weather/capsule.cmake).
Adding a weather provider
Section titled “Adding a weather provider”The weather capsule gets its data from a provider, a
weather_capsule_provider (capsules/weather/weather_provider.h): a name and a
blocking fetch(get, latitude, longitude, out_map) that fills the initialized map
out_map with temperature (double, Celsius) using the supplied HTTP get
function, returning non-OK on any failure. To add one:
- Implement it in its own file, e.g.
capsules/weather/weather_<name>.{c,h}, exporting aweather_capsule_provider. - Add it to the static
weather_providerstable inweather_capsule.c. - Add its name to the
providerchoices in the property table there. - Add the source to
SIG_CAP_EXTRA_SOURCESincapsules/weather/capsule.cmake.
A host program that does not use sighost registers capsules directly:
sig_capsule_registry *reg;sig_capsule_registry_create(®);sig_capsule_registry_register(reg, &my_capsule);Using it
Section titled “Using it”Create configurations/Thing.cfg in your project with "type": "mything" and any attr_value
properties, then list Thing in a service’s execution array. See
configuration.md.
Testing
Section titled “Testing”Unit tests use cmocka and live beside the capsule
(capsules/<name>/test/test_<name>_capsule.c); the build picks them up
automatically when SIG_TEST is on. Both built-ins expose small helper
functions “for unit testing only” (simulator_next_value,
counter_report_and_reset) so logic can be tested without a running service.