A module's behaviour is implemented as one or more cranks: exported C/C++ functions that the system calls when the module triggers. This page covers the crank contract, the cmlabs::PsyAPI handle, the two crank styles, and the concurrency rules you must respect.
The crank contract
A crank library declares its cranks extern "C" and exports them:
extern "C" {
}
}
PsyAPI — the component-facing API handle of the CMSDK.
The return value is a signed 8-bit status, normally 0 (currently ignored, reserved for future use). The PsySpec binds a module's crank to the function by library and name:
<library name="Examples" library="Examples" />
<module name="Ping">
<trigger name="Ball" type="ball.1" />
<crank name="Ping" function="Examples::Ping" />
<post name="Ball" type="ball.2" />
</module>
- Note
- Crank libraries are loaded and unloaded dynamically. Do not rely on global or static mutable state in crank code — apart from the concurrency issues below, dynamic loading can break their semantics entirely, and on some POSIX systems libraries with such symbols can fail to load at all.
The PsyAPI handle
The cmlabs::PsyAPI object passed to the crank is its only window onto the system. The core loop of nearly every crank is:
int8 MyCrank(PsyAPI* api) {
const char* triggerName;
DataMessage* inMsg;
while (api->shouldContinue()) {
if ((inMsg = api->waitForNewMessage(100, triggerName))) {
api->postOutputMessage();
}
}
return 0;
}
The essential families of calls (all on cmlabs::PsyAPI):
- Lifecycle — cmlabs::PsyAPI::shouldContinue() asks whether to keep running; return promptly when it goes false (system shutdown or context loss).
- Input — cmlabs::PsyAPI::waitForNewMessage() blocks up to a timeout (ms) for the next trigger and reports the trigger name from the PsySpec, so one crank can serve several inputs: if (stricmp(triggerName, "Ball") == 0) .... Related: cmlabs::PsyAPI::getCurrentTriggerName(), cmlabs::PsyAPI::getCurrentTriggerContext().
- Output — cmlabs::PsyAPI::postOutputMessage(postName, msg). With no arguments it posts through all active <post> entries; with a name it posts through the matching entries only; the message argument is optional (an empty message is created if omitted). Negative return values are errors (POST_FAILED, POST_NOSPEC, POST_OUTOFCONTEXT); non-negative is the number of messages posted.
- Signals — cmlabs::PsyAPI::emitSignal() / cmlabs::PsyAPI::waitForSignal() for named system-wide broadcasts (see Core Concepts).
- Parameters — cmlabs::PsyAPI::hasParameter(), cmlabs::PsyAPI::getParameter(), cmlabs::PsyAPI::getParameterString(), cmlabs::PsyAPI::tweakParameter() etc. read and adjust the parameters declared for this module in the PsySpec — the standard way to make a crank configurable without recompiling:
int64 cycles = 10;
if (api->hasParameter("Cycles"))
api->getParameter("Cycles", cycles);
- Retrieves and queries — cmlabs::PsyAPI::retrieve() pulls stored messages from a whiteboard, cmlabs::PsyAPI::queryCatalog() reads/writes catalog data; both execute named specs declared in the PsySpec (see Whiteboards & Catalogs).
- Logging — cmlabs::PsyAPI::logPrint(level, fmt, ...) prints to the system log with printf-style formatting; the level is used for debug filtering.
- Identity — cmlabs::PsyAPI::getModuleName(), cmlabs::PsyAPI::typeToText() and friends.
Messages returned by waitForNewMessage()/waitForSignal() are owned by the system — never delete them. Messages you create with new DataMessage() are handed over when posted.
One-shot vs. long-running cranks
A crank can be written in two styles, and the difference matters:
- One-shot: do the work for a single trigger and return — like Examples::Simple in Examples/src/Examples.cpp. The system calls the function again for the next firing.
- Long-running: stay inside the function looping on cmlabs::PsyAPI::shouldContinue(), handling trigger after trigger — like Examples::Ping. This keeps per-message overhead minimal and lets the crank hold local state (counters, timers) in stack variables across messages.
- Warning
- Concurrency. A one-shot crank borrows a pool thread per trigger firing (see cmlabs::ThreadManager for the threading model). If triggers fire faster than the crank completes, the same crank function can be executing concurrently on several threads at once — and the same function can also be bound to several modules. Any state shared between invocations (globals, statics, shared caches) must be independently synchronised, e.g. with utils::Mutex. The safest pattern is to keep all state either local to the function or inside messages and whiteboards.
Worked example: Ping
The ping-pong benchmark crank (Examples::Ping, PsySpec Examples/pingpong.xml) shows the standard shape with named triggers, named posts, parameters and logging:
int8 Ping(PsyAPI* api) {
api->logPrint(1, "Started running (internal)...");
DataMessage *inMsg, *outMsg;
const char* triggerName;
int64 counter = 0, cycles = 10;
if (api->hasParameter("Cycles"))
api->getParameter("Cycles", cycles);
while (api->shouldContinue()) {
if ((inMsg = api->waitForNewMessage(100, triggerName))) {
outMsg = new DataMessage();
if (
stricmp(triggerName,
"Ready") == 0) {
}
else if (
stricmp(triggerName,
"Ball") == 0) {
if (++counter % 100000 == 0) {
api->postOutputMessage("Done", outMsg);
outMsg = NULL;
}
}
if (outMsg)
api->postOutputMessage("Ball", outMsg);
}
}
return 0;
}
(Abridged; see Examples/src/Examples.cpp for the full version with microsecond timing statistics via GetTimeNow() and GetTimeAge() — Functions dealing with time.)
External modules: cranks outside the node
Cranks do not have to be loaded into a Psyclone-managed space. Any stand-alone executable can attach itself to a running node by creating its own cmlabs::PsySpace, connecting, and asking for a crank API by name:
PsySpace* space = new PsySpace("MyProcess");
space->connect(10000);
PsyAPI* api = space->getCrankAPI("Test.RetrieveTest");
while (space->isConnected() && !space->hasShutdown()) {
if (DataMessage* msg = api->waitForNewMessage(100)) {
}
}
The module is still declared in the PsySpec (so its wiring stays in one place); only its code runs elsewhere. Examples/src/ExternalModules.cpp is a complete external-module program, and Examples/externals.xml a matching PsySpec — including reconnect handling for when the node goes away. Also see the space= attribute in Distributed Systems for the converse: letting the node place a normal module into a separate OS process for you.
Checklist
- Loop on cmlabs::PsyAPI::shouldContinue(); exit promptly when it returns false.
- Never delete trigger/signal messages; always post or delete messages you create.
- No mutable globals/statics; guard anything shared (one-shot cranks run concurrently).
- Read tunables from parameters, not constants.
- Use cmlabs::PsyAPI::logPrint() with levels rather than printf.
- Keep wiring in the PsySpec: post by name, never hardcode destinations.