Skip to content

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.

#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 */
};
  • name is what a capsule file’s "type" refers to.
  • Input terminals need a handler; output terminals set it to NULL.
  • category says 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). NULL leaves 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.
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).

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 required or a default, an optional int range, optional string choices (matched case-insensitively), and a one-line description. The SIG_PROP_* macros in sig_capsule.h build 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 in start, 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, but PUT/PATCH of 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_resolve on the instance first.
  • A capsule with no table (num_properties of 0) accepts any properties and reads them with sig_capsule_configuration_get_property(inst->configuration, key, &v); prefer a table.
  • Storage is per-instance mutable state of sig_value type: sig_capsule_instance_set_storage / get_storage. set shares the value (you still own and must sig_value_dtor yours); get returns a borrowed pointer that is invalidated by the next set on 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.

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.

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).

  1. Create capsules/<name>/<name>_capsule.h and .c, exporting extern const sig_capsule <name>_capsule;. Every capsule gets its own folder.
  2. Add <name> to the SIG_CAPSULES CMake list (default counter;simulator;logger;filter;switch;state_change), e.g. -DSIG_CAPSULES="counter;simulator;mything".
  3. Add an #ifdef SIG_CAPSULE_<NAME> block to capsules/sig_builtin_capsules.c that 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_client makes capsules/CMakeLists.txt skip the capsule, with a warning, unless SIG_HTTP_CLIENT is on.
  • SIG_CAP_EXTRA_SOURCES: extra .c files 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 as sig::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).

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:

  1. Implement it in its own file, e.g. capsules/weather/weather_<name>.{c,h}, exporting a weather_capsule_provider.
  2. Add it to the static weather_providers table in weather_capsule.c.
  3. Add its name to the provider choices in the property table there.
  4. Add the source to SIG_CAP_EXTRA_SOURCES in capsules/weather/capsule.cmake.

A host program that does not use sighost registers capsules directly:

sig_capsule_registry *reg;
sig_capsule_registry_create(&reg);
sig_capsule_registry_register(reg, &my_capsule);

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.

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.