CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
Writing a Module (Crank)

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:

// Examples.h
#include "PsyAPI.h"
namespace cmlabs {
extern "C" {
DllExport int8 Simple(PsyAPI* api);
DllExport int8 Ping(PsyAPI* api);
DllExport int8 Print(PsyAPI* api);
}
} // namespace cmlabs
PsyAPI — the component-facing API handle of the CMSDK.
#define DllExport
Definition Utils.h:147

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" /> <!-- .dll/.so resolved per OS -->
<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))) {
// process inMsg — owned by the system, do NOT delete
api->postOutputMessage(); // post per active spec entries
}
}
return 0;
}

The essential families of calls (all on cmlabs::PsyAPI):

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) {
// start of game — reset counters
}
else if (stricmp(triggerName, "Ball") == 0) {
if (++counter % 100000 == 0) {
api->postOutputMessage("Done", outMsg); // e.g. shutdown post
outMsg = NULL;
}
}
if (outMsg)
api->postOutputMessage("Ball", outMsg); // return the ball
}
}
return 0;
}
#define stricmp
Definition Utils.h:132

(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); // node's port = system id
PsyAPI* api = space->getCrankAPI("Test.RetrieveTest"); // Module.Crank
while (space->isConnected() && !space->hasShutdown()) {
if (DataMessage* msg = api->waitForNewMessage(100)) {
// ... exactly the same PsyAPI as internal cranks ...
}
}

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.