Apis
Reactive, session-oriented, asynchronous process-calculus framework
This crate provides traits and a macro interface for defining sessions of reactive threads ("processes" in the sense of 'process calculus'), communicating messages over a fixed topology of channels. It also provides a macro for defining a "program" as a state transition system on session nodes, called "modes", in which thread-local state may be passed from processes of one mode to the next.
Current features
- Initialization-time validation of process and channel connectivity and consistency within a session
- Asynchronous sends and unbounded channels
- Three kinds of channels classified by connection topology:
- 'Simplex' -- a one-way SPSC stream
- 'Sink' -- a standard MPSC channel
- 'Source' -- an SPMC unicast channel
- Four kinds of processes with varying receive and update behavior, roughly
divided into two classes by receive mechanism:
- "asynchronous" -- block-waiting receives for a single endpoint ('Asynchronous')
- "synchronous" -- polling loops on any number of endpoints with various timing schemes ('Isochronous', 'Mesochronous', 'Anisochronous')
- Logging of events using the
log
logging API - Graphviz DOT file output for session data flow diagrams and program state transition diagrams.
Current limitations
- Process and channel definitions are local to a particular session definition; they cannot be re-used. One way to partly mitigate this limitation is to create a context type that encapsulates the intended 'role' and re-use that in multiple processes.
- Passing state between sessions is implemented in a continuation-passing style and the Rust compiler cannot not optimize away the tail recursion in this case; note this only occurs if state is explicitly transferred between sessions, otherwise unaffected threads will join back with the main thread and no tail recursion will take place.
Usage
The features of this library are implemented using two top-level macro
definitions, def_session!
and def_program!
, to define sessions and
programs, respectively.
Sessions
The def_session!
macro expands to datatype and function implementations that
rely on two unstable features, const_fn
and try_from
:
Define a session 'IntSource' in which a source thread sends u64
values
alternatively to two peers which sum the received values and return a final sum
in the session result:
Note that it is necessary to introduce variable identifiers (here process
and
message_in
) in the session definition so that they can be referred to in
handle_message
and update
blocks, or in optional initialize
and
terminate
blocks (not shown). Here the identifier process
will be made a
mutable self reference to the local process in each block, and message_in
will be made an alias for the received message in the scope of handle_message
blocks only.
Generate a graphviz DOT file representing the session data flow diagram and write to file:
let session_def = def.unwrap;
use Write;
let mut f = create .unwrap;
f.write_all .unwrap;
drop ;
Rendered as PNG with $ dot -Tpng intsource.dot > intsource.png
:
Note that sessions define a number of types in the scope where the macro is invoked. Putting each session in its own module allows them to be sequentially composed into "programs", described next.
Programs
When defining a program using the def_program!
macro, two additional unstable
features, core_intrinsics
and fnbox
, are required:
Define another session CharSink
in module char_sink
with different behavior
and reversed message flow (implementation omitted, see ./examples/readme.rs
):
A program can then be defined which runs both sessions sequentially:
def_program!
Note that it is necessary to introduce the result
identifier here to access
the result of a session.run()
call within the (optional) 'transition
choice block' associated to a mode, in this case 'IntSource'. Here the
transition is always the same, however the contents of the session result can
be used to nondeterministically choose any transition with a source matching
the finished session. If no transition choice block is defined (as is the case
with 'CharSink' above), or if a transition choice block evaluates to 'None',
then the program will exit and not transition to any other session.
For examples of programs that transfer state from processes of one session to
the next, see program.rs
, interactive.rs
, or graphical.rs
in the
./examples/
directory.
A program is implemented as a state machine for which a DOT file can be generated showing the program state transition system:
use Write;
let mut f = create .unwrap;
f.write_all .unwrap;
drop ;
Logging
The log
crate is used to provide log messages at various levels which are
ignored unless a logging implementation is selected, for example simplelog
:
[]
= "0.5.*"
Using a TermLogger
to display log messages in the terminal:
extern crate simplelog;
Examples
A number of example programs are given in ./examples/
. Non-interactive
examples can be run by the ./run-examples.sh
script which will also builds
images from generated DOT files. The graphical.rs
and interactive.rs
examples are interactive, requiring user input. These can be run with the
./run-interactive.sh
script which will also produce images from the generated
DOT files for these examples.
Most of these examples will intentionally generate warnings, see the doc comments of individual examples for specifics.
Running tests
Doctests of process and channel definitions need to be run with --features "test"
to compile successfully:
$ cargo test --features "test"