Publish-Subscribe (psub)
A system-level publish-subscribe message bus based on topic-based loosely coupled communication, enabling modules to exchange data without direct references, achieving complete decoupling between producers and consumers.
Decoupling Benefits
Traditional inter-module communication typically uses direct function calls, creating compile-time dependencies and runtime coupling between modules:
- Direct Call: Module A calls module B's function; A must include B's header, and B must initialize before A
- Publish-Subscribe: Module A publishes a message to topic
"topic-x", module B subscribes to"topic-x"; A and B are unaware of each other, no header inclusion needed, initialization order is irrelevant
This decoupling provides the following advantages:
- Eliminated Dependencies: No compile-time dependency between publisher and subscriber; each compiles and evolves independently
- One-to-Many Communication: A single published event can notify multiple subscribers simultaneously, without the publisher maintaining a subscriber list
- Dynamic Subscription: Subscribers can subscribe and unsubscribe at runtime, flexibly responding to system state changes
- Lifecycle Decoupling: If there are no subscribers when publishing, the event is silently dropped; once subscribed, new publications are received immediately
- One-shot Subscription: The
oneshotflag supports a "subscribe once, auto-unsubscribe" pattern, useful for initialization wait scenarios
How It Works
Internal Structure
psub is a system-level wrapper around libx/ps, creating a global psctx_t context during the pure_initcall phase:
psctx_t (global context)
├── Hash table (topic index)
│ ├── "wifi.connected" → pstopic_t
│ │ ├── pscb_t (subscriber 1: cb1, sdat1)
│ │ └── pscb_t (subscriber 2: cb2, sdat2)
│ └── "sensor.data" → pstopic_t
│ └── pscb_t (subscriber 3: cb3, sdat3)
└── Mutex
Topic Management
- Topics are strings, indexed via string hashing (
shash) - The hash table auto-expands: when the number of topics exceeds half the hash table capacity, the capacity doubles
- Topics are created automatically on first subscription and retained when there are no subscribers (not proactively deleted)
Publish Flow
The processing in psub_publish():
- Look up the topic in the hash table; if not found, return immediately (event dropped)
- Splice the subscriber callback list to a local list (outside lock protection)
- Call each subscriber callback:
cb(pdat, sdat) - After callbacks complete, re-acquire the lock and return non-oneshot subscribers to the list
- Free memory for oneshot subscribers
This "splice first, then callback" design avoids deadlocks when callbacks publish or subscribe to the same topic again.
Subscription Deduplication
If a subscription with the exact same cb and sdat already exists on the same topic, the new subscription request is ignored to prevent duplicate callbacks.
API
| Function | Description |
|---|---|
psub_publish(topic, pdat) | Publish a message to a topic; pdat is the data pointer passed to subscribers |
psub_subscribe(topic, cb, sdat, oneshot) | Subscribe to a topic; cb is the callback, sdat is subscriber private data, oneshot of 1 auto-unsubscribes after one trigger |
psub_unsubscribe(topic, cb, sdat) | Unsubscribe; cb of NULL unsubscribes all subscribers on that topic |
Callback prototype: void cb(void * pdat, void * sdat)
pdat: Data passed by the publishersdat: Private data provided at subscription time
Usage Examples
Basic Publish-Subscribe
#include <kernel/core/psub.h>
/* Subscriber */
static void on_wifi_connected(void * pdat, void * sdat)
{
const char * ssid = (const char *)pdat;
struct my_app_t * app = (struct my_app_t *)sdat;
LOG("wifi connected: %s\n", ssid);
app->wifi_ready = 1;
}
void app_init(void)
{
struct my_app_t * app = get_app();
psub_subscribe("wifi.connected", on_wifi_connected, app, 0);
}
/* Publisher (another module, no need to know about app) */
void wifi_driver_notify(void)
{
psub_publish("wifi.connected", "MyNetwork");
}
One-shot Subscription (Initialization Wait)
/* Wait for a service to be ready, notify only once */
static void on_service_ready(void * pdat, void * sdat)
{
LOG("service is ready, starting dependent module\n");
start_my_module();
}
void init_dependent_module(void)
{
/* oneshot = 1, auto-unsubscribes and frees memory after triggering */
psub_subscribe("service.ready", on_service_ready, NULL, 1);
}
Unsubscribe
/* Precise unsubscribe */
psub_unsubscribe("sensor.data", my_callback, my_data);
/* Unsubscribe all subscribers on this topic */
psub_unsubscribe("sensor.data", NULL, NULL);
Multiple Subscribers
/* Display module: subscribe to battery level to update UI */
static void on_battery_update(void * pdat, void * sdat)
{
int level = *(int *)pdat;
ui_set_battery_level(level);
}
psub_subscribe("battery.level", on_battery_update, NULL, 0);
/* Logging module: subscribe to battery level to log */
static void on_battery_log(void * pdat, void * sdat)
{
int level = *(int *)pdat;
LOG("battery: %d%%\n", level);
}
psub_subscribe("battery.level", on_battery_log, NULL, 0);
/* Power driver: publish once, both subscribers receive it */
int level = 85;
psub_publish("battery.level", &level);
Notes
psubautomatically initializes a global context during thepure_initcall()phase; no manual invocation is needed- All operations are protected by mutexes, making them thread-safe
- If there are no subscribers when publishing, the event is silently dropped without error
- Callbacks execute synchronously in the publisher's thread; avoid time-consuming operations in callbacks
- Callbacks can safely call
psub_publish()orpsub_subscribe()(no deadlock) oneshotsubscriptions automatically free memory after triggering; no manual unsubscribe needed- In
psub_unsubscribe(), passingcbas NULL unsubscribes all subscribers on that topic - The underlying implementation is based on
libx/ps, which supports independentpsctx_tcontexts for isolating different publish-subscribe domains