Overview
Implementing simple no-frills coroutines is surprisingly easy, due to the way the Astrobe compiler creates code:
- before calling a (non-function) procedure, any computed values in registers have been safely stored, either onto the stack at the location of the corresponding local variables (including value parameters), or into the variables passed as VAR parameters, or into module variables, and
- the procedure prologue and epilogue can directly be used for the coroutine transfer logic.
Control between coroutines is passed using procedure Coroutines.Transfer
. Due to the above, no data registers or computational state need to be preserved upon such a context switch.1
Here’s the most basic implementation. All functionality related to monitoring (stack monitoring, procedure call traces) is omitted for clarity.
TYPE Coroutine
Each coroutine is described by a RECORD:
TYPE
Coroutine* = POINTER TO CoroutineDesc;
CoroutineDesc* = RECORD
sp: INTEGER;
(* ... *)
END;
SP
: stored stack pointer register value
Transfer
CONST SP = 14;
PROCEDURE Transfer*(f, t: Coroutine);
BEGIN
(* prologue *)
f.sp := SYSTEM.REG(SP);
SYSTEM.LDREG(SP, t.sp)
(* epilogue *)
END Transfer;
Neglecting for now how to initially set up the coroutine to make this work, let’s assume we have two running coroutines, both have done transfers before (“steady state”), and we want to transfer control from f
to t
, that is, coroutine f
calls Coroutine.Transfer(f, t)
: 2
- Upon entry, the stack of
f
is in use. The prologue pushes the LNK register value, and parametersf
andt
onto that stack. The stack pointer value is decreased by 12 (four bytes for LNK,f
, andt
each). - The SP register value is saved as
f.sp
. - The SP register gets set to
t.sp
. At this point,t
is still accessible on the stack off
.t.sp
has been set when coroutinet
did its last transfer, ie. when it had the role off
, see the preceding step. - Now, the stack of
t
is used. - The epilogue restores the LNK register value from the stack of
t
, and adjusts the stack pointer value by +12. The LNK value has been set when coroutinet
did its last transfer, ie. by the corresponding prologue. - The epilogue branches to LNK, hence
t
continues right after at the point when it last calledTransfer
.
Note that any kind of coroutine status data could be pushed onto the stack before the switch if ever needed.
Initialisation
For the first invocation via Transfer
, the coroutine stack needs to be set up, to a state as if a Coroutines.Transfer
had been done before, in order for the first transfer to the new coroutine to work.
PROCEDURE Init*(cor: Coroutine; code: PROCEDURE; stackAdr, stackSize: INTEGER);
BEGIN
(* ... *)
cor.sp := stackAdr + stackSize;
DEC(cor.sp, 12);
SYSTEM.PUT(cor.sp, SYSTEM.VAL(INTEGER, code));
END Init;
- Set the stored stack pointer value
cor.sp
to the top of the stack memory area. - Decrement
cor.sp
to allow the “+12” adjustment in the epilogue of the initialTransfer
. - Store the coroutine code address on the stack, ie. at the position of the LNK register value.
The stack of the new coroutine is now in a state to allow to be transferred-to from another coroutine.
The First Coroutine
All system and control processes are invoked by the scheduler coroutine, Processes.loop
, using Coroutines.Transfer
, initially for new coroutines (processes), and then in steady state. Hence we need to set up this first coroutine at the end of the start-up sequence.
Module Processes
creates the coroutine RECORD for the scheduler:
VAR loop: Coroutines.Coroutine.
(* ... *)
NEW(loop); ASSERT(loop # NIL);
(* ... *)
Module Oberon
calls Processes.Go
at the end of the start-up sequence:3
PROCEDURE Go*;
VAR jump: Coroutines.Coroutine;
BEGIN
SYSTEM.LDREG(SP, SchedulerStackTop - 32);
NEW(jump);
Coroutines.Init(loop, Loop, SchedulerStackBottom, SchedulerStackSize);
Coroutines.Transfer(jump, loop)
END Go;
jump
is just a temporary data structure to allow this initial transfer. It does not need to be initialised, as we’ll never transfer back to jump
, in fact we could not, as it will be collected.
Furthermore, as described here, the stack used by the startup process, including the body of module Oberon
, is being reallocated to the scheduler coroutine and the commands handling process. Hence, the stack used by Processes.Go
will be overwritten by Coroutines.Init
. Therefore, the stack pointer is set to not use the top part of the scheduler stack.
-
With Astrobe for the Cortex processors, this would not be true if ‘Coroutines.Transfer’ were declared a leaf procedure. Hence in the Cortex version there is a NOTE!, saying “No, this cannot be a leaf procedure!”, since one could be tempted… :) ↩︎
-
Remember that coroutine transfers are always between explicitly known coroutines. For “anonymous” yielding, a scheduler is required. ↩︎
-
As does module
Errors
at the end of the error-recovery procedure. ↩︎