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
log
logging APICurrent limitations
The features of this library are implemented using two top-level macro
definitions, def_session!
and def_program!
, to define sessions and programs,
respectively.
Internally these macros make use of the enum_iterator::IntoEnumIterator
derive
macro which requires that enum-iterator
is imported in Cargo.toml
:
apis = "0.4"
enum-iterator = "0.7"
Sessions
The def_session!
macro expands to datatype and function implementations
defining processes, channels, and messages.
Example
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:
```rust extern crate apis;
pub mod int_source { use apis;
const MAX_UPDATES : u64 = 10;
apis::defsession!{
context IntSource {
PROCESSES where
let process = self,
let messagein = messagein
[
process IntGen (updatecount : u64) {
kind { apis::process::Kind::Isochronous { tickms: 20, ticksperupdate: 1 } }
sourcepoints [Ints]
endpoints []
handlemessage { unreachable!() }
update { process.intgenupdate() }
}
process Sum1 (sum : u64) -> (u64) {
kind { apis::process::Kind::asynchronousdefault() }
sourcepoints []
endpoints [Ints]
handlemessage { process.sum1handlemessage (messagein) }
update { apis::process::ControlFlow::Continue }
}
process Sum2 (sum : u64) -> (u64) {
kind { apis::process::Kind::asynchronousdefault() }
sourcepoints []
endpoints [Ints]
handlemessage { process.sum2handlemessage (messagein) }
update { apis::process::ControlFlow::Continue }
}
]
CHANNELS [
channel Ints
impl IntGen { pub fn intgenupdate (&mut self) -> apis::process::ControlFlow { use apis::Process; use num::FromPrimitive; let toid = (self.updatecount % 2) + 1; let anint = self.updatecount; let mut result = self.sendto ( ChannelId::Ints, ProcessId::fromu64 (toid).unwrap(), Intsmessage::Anint (anint) ).into(); self.updatecount += 1; if result == apis::process::ControlFlow::Break || MAXUPDATES < self.updatecount { // quit let _ = self.sendto (ChannelId::Ints, ProcessId::Sum1, Intsmessage::Quit); let _ = self.send_to (ChannelId::Ints, ProcessId::Sum2, Intsmessage::Quit); result = apis::process::ControlFlow::Break } result } }
impl Sum1 { fn sum1handlemessage (&mut self, message : GlobalMessage) -> apis::process::ControlFlow { match message { GlobalMessage::Intsmessage (Intsmessage::Anint (anint)) => { self.sum += anint; apis::process::ControlFlow::Continue } GlobalMessage::Intsmessage (Intsmessage::Quit) => { self.result = self.sum; apis::process::ControlFlow::Break } } } }
impl Sum2 { fn sum2handlemessage (&mut self, message : GlobalMessage) -> apis::process::ControlFlow { match message { GlobalMessage::Intsmessage (Intsmessage::Anint (anint)) => { self.sum += anint; apis::process::ControlFlow::Continue } GlobalMessage::Intsmessage (Intsmessage::Quit) => { self.result = self.sum; apis::process::ControlFlow::Break } } } } }
fn main() {
use intsource::*;
use apis::session::Context;
// verifies the validity of the session definition
let sessiondef = IntSource::def().unwrap();
// create the session in the 'Ready' state
let mut session : apis::Session
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:
rust
let session_def = IntSource::def().unwrap();
use std::io::Write;
let mut f = std::fs::File::create ("intsource.dot").unwrap();
f.write_all (session_def.dotfile().as_bytes()).unwrap();
drop (f);
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
Example
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:
```rust
apis::defprogram! {
program Myprogram where let result = session.run() {
MODES [
mode intsource::IntSource {
use apis::Process;
let sum1 = intsource::Sum1::extractresult (&mut result).unwrap();
let sum2 = intsource::Sum2::extractresult (&mut result).unwrap();
println!("combined sums: {}", sum1 + sum2);
Some (EventId::ToCharSink)
}
mode charsink::CharSink
]
TRANSITIONS [
transition ToCharSink
fn main() { use apis::Program; // create a program in the initial mode let mut myprogram = Myprogram::initial(); // run to completion myprogram.run(); } ```
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:
rust
use std::io::Write;
let mut f = std::fs::File::create ("myprogram.dot").unwrap();
f.write_all (Myprogram::dotfile().as_bytes()).unwrap();
drop (f);
The log
crate is used to provide log messages at various levels which are
ignored unless a logging implementation is selected, for example simplelog
:
toml
[dependencies]
simplelog = "0.6.*"
Using a TermLogger
to display log messages in the terminal:
rust
extern crate simplelog;
fn main() {
simplelog::TermLogger::init (
simplelog::LevelFilter::Debug,
simplelog::Config::default(),
simplelog::TerminalMode::Stdout
).unwrap();
// ...
}
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.
Doctests of process and channel definitions need to be run with --features
"test"
to compile successfully:
$ cargo test --features "test"
(see https://github.com/rust-lang/rust/issues/45599).