The Active Object pattern separates the invocation of an operation from its execution. Instead of calling directly into an object that might be concurrently accessed, a client submits a request and continues. The object owns its thread of control and executes requests sequentially in that thread.
That is the essential distinction made in the original Active Object pattern: method execution is decoupled from method invocation.
The pattern has close relatives in message-oriented systems. Erlang processes, for example, communicate asynchronously through process mailboxes. In the Embedded realm we are often talking about event-driven, run-to-completion tasks and state machine patterns. These models are related rather than identical, but they share a useful architectural property: state belongs to the active entity, while other execution contexts interact with it by sending information rather than entering its implementation directly.
The pattern in RK0 terms
An RK0 Active Object can be reduced to three elements:
asynchronous message
Client A ----------------------------\
\
Client B -----------------------------> Active Object Task
[ private state ]
Client C -----------------------------> [ dispatch loop ]
/
/
kMesgWait()
Messages are addressed directly to its task handle. The endpoint state and pending-message list belong to the receiver TCB. kMesgSend() transfers a pool-backed RK_MESG to that endpoint and does not block waiting for receiver queue space.
That distinction is important:
kMesgAlloc() reserve bounded message storage | v fill payload | vkMesgSend() transfer ownership to AO | +---------- caller continues AO eventually: | v kMesgWait() | v dispatch | v kMesgFree()
A successful kMesgSend() does not mean that the Active Object has executed the request. It means that ownership of the message has transferred to the receiving endpoint. The sender must no longer access or free it.
That is precisely the asynchronous boundary we want.
A complete Active Object
Consider a small filter component. Other tasks can change its gain, submit samples, or reset it, but they never access the filter state directly.
The Active Object owns:
typedef struct{ LONG gain; LONG accumulator;} FilterState_t;
Nobody else gets a pointer to FilterState_t.
Commands crossing the asynchronous boundary are represented separately:
typedef enum{ FILTER_CMD_SET_GAIN = 0, FILTER_CMD_UPDATE, FILTER_CMD_RESET} FilterCmd_t;typedef struct{ FilterCmd_t cmd; LONG value;} FilterMesg_t;
We declare the task and a fixed message pool:
STACKSIZE FILTER_POOL_DEPTHRK_DECLARE_TASK(filterHandle, FilterTask, filterStack, STACKSIZE)RK_DECLARE_MESG_POOL(filterPool, filterPoolBuf, FilterMesg_t, FILTER_POOL_DEPTH)
RK_DECLARE_MESG_POOL() reserves statically bounded storage for RK_MESG headers plus the corresponding payloads. The current API computes the necessary block size from the message type.
Initialisation is straightforward:
VOID kApplicationInit(VOID){ RK_ERR err = kTaskInit(&filterHandle, FilterTask, RK_NO_ARGS," "Filter", filterStack, STACKSIZE, 4U, RK_PREEMPT); K_ASSERT(err == RK_ERR_SUCCESS); err = kMesgPoolInit(&filterPool, filterPoolBuf, sizeof(FilterMesg_t), FILTER_POOL_DEPTH, RK_MESG_PRIO_CEILING_NONE); K_ASSERT(err == RK_ERR_SUCCESS); err = kMesgEndpointInit(filterHandle); K_ASSERT(err == RK_ERR_SUCCESS);}
The task is first created normally with kTaskInit(). kMesgEndpointInit() then gives that task its asynchronous receiving endpoint. The current API also retains kCreateTask as an alias for kTaskInit().
Notice that the example disables the message-pool priority ceiling. We will return to that point shortly.
Posting an operation
The public interface of the Active Object can be a small function:
static RK_ERR FilterPost(FilterCmd_t const cmd, LONG const value){ RK_MESG *mesgPtr = NULL; RK_ERR err = kMesgAlloc(&filterPool, &mesgPtr, RK_NO_WAIT); if (err != RK_ERR_SUCCESS) { return (err); } FilterMesg_t *payloadPtr = RK_MESG_PAYLOAD(mesgPtr, FilterMesg_t); payloadPtr->cmd = cmd; payloadPtr->value = value; err = kMesgSend(filterHandle, mesgPtr); if (err != RK_ERR_SUCCESS) { RK_ERR const freeErr = kMesgFree(mesgPtr); K_ASSERT(freeErr == RK_ERR_SUCCESS); } return (err);}
A client can now write:
RK_ERR err = FilterPost(FILTER_CMD_SET_GAIN, 4);if (err != RK_ERR_SUCCESS){ /* application-defined overload/error policy */}
and later:
err = FilterPost(FILTER_CMD_UPDATE, sample);
There are two operations here that should not be confused.
kMesgAlloc() acquires one finite communication resource from the pool. We pass RK_NO_WAIT, so an exhausted pool causes the operation to fail immediately with RK_ERR_BUFFER_EMPTY.
kMesgSend(), on the other hand, transfers the already allocated message to the task endpoint. It has no timeout parameter because it does not wait for endpoint queue capacity.
Therefore the strict asynchronous AO interface is:
allocate without waiting +send without waiting for receiver
not merely:
send(timeout = 0)
That is a cleaner semantic match than a queue-based implementation.
The Active Object task
The receiving side is equally small:
VOID FilterTask(VOID *args){ RK_UNUSEARGS FilterState_t state = {1, 0}; while (1) { RK_MESG *mesgPtr = NULL; RK_ERR err = kMesgWait(RK_ANY_TASK, &mesgPtr, RK_WAIT_FOREVER); K_ASSERT(err == RK_ERR_SUCCESS); FilterMesg_t const *payloadPtr = RK_MESG_PAYLOAD(mesgPtr, FilterMesg_t); switch (payloadPtr->cmd) { case FILTER_CMD_SET_GAIN: { state.gain = payloadPtr->value; break; } case FILTER_CMD_UPDATE: { state.accumulator += payloadPtr->value * state.gain; break; } case FILTER_CMD_RESET: { state.accumulator = 0; break; } default: { break; } } err = kMesgFree(mesgPtr); K_ASSERT(err == RK_ERR_SUCCESS); }}
The concurrency rule is now visible in the code:
many execution contexts | | messages v+-----------------------+| FilterTask || || FilterState_t state || || receive -> dispatch || receive -> dispatch || receive -> dispatch |+-----------------------+
There is no mutex around state because there is no concurrent access to it. Only FilterTask manipulates it.
This is one of the central benefits of the Active Object pattern: synchronisation is largely replaced by serialisation through the object’ss execution context.
Preemptive, but Run-to-Completion
RK0 is preemptive, but its execution model is compatible with run-to-completion (RTC) processing.
Preemption does not mean that an Active Object abandons the message it is currently handling. A higher-priority task may preempt it at any instruction. When that task ceases to be the highest-priority READY task, the preempted Active Object resumes from the same point and continues processing the same message.
while (1){ kMesgWait(RK_ANY_TASK, &mesgPtr, RK_WAIT_FOREVER); dispatch(mesgPtr); /* one RTC reaction */ kMesgFree(mesgPtr);}
dispatch() may be preempted by more urgent work, but the Active Object cannot process its next message until dispatch()returns. Preemption therefore changes when the RTC step executes, not which reaction is in progress.
Priority ceiling for Message Pools does not fit AO patterns
On an AO pattern the sender normally does not block.
The application therefore has an explicit overload boundary if using RK_NO_WAIT
err = kMesgAlloc(&filterPool, &mesgPtr, RK_NO_WAIT);if (err == RK_ERR_BUFFER_EMPTY){ /* AO admission failed: handle it */}
Thus, initialising a Message Pool with RK_MESG_PRIO_CEILING_NONE is the natural default for AO patterns.
Summary: RK0 interpretation
Active Object concept RK0 mechanism-----------------------------------------------------------active execution context Taskaddress RK_TASK_HANDLEasynchronous inbox Direct Message endpointevent/command storage RK_MESG poolasynchronous activation kMesgSend()waiting for activation kMesgWait()private state task-local/application statebounded admission kMesgAlloc()resource exhaustion policy allocation timeout / RK_NO_WAITpool inversion control optional message priority ceiling
The Active Object therefore does not require a special RK0 object. It is a natural fit.
The task itself is the endpoint; the message carries ownership; the pool makes resource limits explicit; and the caller can submit work without waiting for the receiver to consume it.
That is a much closer correspondence between what the application means and what the kernel is asked to do.

Leave a Reply