Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Embedded Scripting Systems

You can use Graphix as an embedded scripting engine in your application. For this application we leave the shell behind and move to the graphix-rt crate. This is a lower level crate that gives you a lot more control over the compiler and runtime.

Tokio

graphix-rt uses tokio internally. It will run the compiler and run loop in its own tokio task. You interface with this task via a GXHandle. As a result, all operations on Graphix objects are async, and the compiler/run loop will run in parallel with your application.

Setting it up

The shell itself is the best example of using the graphix-rt crate. As we get further into internals, the details can change more often, but in general getting a Graphix runtime going involves the following:

#![allow(unused)]
fn main() {
use graphix_rt::{GXConfig, GXRt, NoExt};
use tokio::sync::mpsc;

// set up an execution context using the generic runtime with no customization
let mut ctx = GXRt::<NoExt>::new_state()?;
// ... use the context to register all your built-ins, etc

// set up a channel to receive events from the RT
let (tx, rx) = mpsc::channel(100);
// build the config and start the runtime
let handle = GXConfig::builder(ctx, tx)
    // set up a root module. Note if you want the standard library you must
    // register packages or load modules manually. Otherwise you will get
    // the bare compiler with no standard library.
    .root(arcstr::literal!("mod mymod"))
    .build()?
    .start().await?;
}

Once that all succeeds you have a running compiler/run loop, and a handle that can interact with it. You are expected to read the rx portion of the mpsc channel. If you do not, the run loop will block waiting for you to read once the mpsc channel fills up.

Compiling Code, Getting Results

Once setup is complete, lets compile some code and get some results! To compile code we call compile on the handle. This results in one or more toplevel expressions and a copy of the environment (or a compile error).

#![allow(unused)]
fn main() {
let cres = handle.compile(arcstr::literal!("2 + 2")).await?;

// in this case we know there is just one top level expression
let e = &cres.exprs[0];

// the actual result will come to us on the channel. If the expression kept
// producing results, we'd keep seeing updates for its id on the channel.
// This is a batch of GXEvents
let mut batch = rx.recv().await.ok_or_else(|| anyhow::anyhow!("the runtime is dead"))?;
let mut env = handle.get_env().await?;
for ev in batch.drain(..) {
    match ev {
        graphix_rt::GXEvent::Updated(id, v) if id == e.id => println!("2 + 2 = {v}"),
        graphix_rt::GXEvent::Env(e) => env = e,
        _ => ()
    }
}
}

Refs and TRefs, Depending on Graphix Variables

If you want to be notified when a variable in Graphix updates you can register a Ref, or a TRef if you have a corresponding Rust type that implements FromValue. There are two ways to get a ref, by id and by name. By id is probably the most common, because BindId will appear in any data structure that has a value passed by ref (e.g. &v), which should be common for large structures that don’t change often.

A Rust type that mirrors a Graphix type need not implement FromValue by hand: #[derive(FromValue, IntoValue)] from the netidx-derive crate maps a struct’s named fields onto the Graphix struct’s fields by name, and an enum’s variants onto Graphix variants (`Tag, `Tag(a, b), `Tag({ .. })). A field passed by reference (&T) reads as a BindId, a reference’s own value (graphix_compiler::reference::cell reads one from a Value).

Every derive over a Graphix value, in either direction, carries #[value(shape = "graphix_compiler::rep::GraphixShape")]. A Graphix tuple, struct or variant value carries a kind marker the default (netidx) shape does not know: without the attribute, FromValue refuses every struct, tuple and variant the program sends, and IntoValue builds values the program does not read as one. The generated code names netidx_value, anyhow and graphix_compiler, so the crate depends on all three.

#![allow(unused)]
fn main() {
// the Graphix type {x: f64, y: f64, label: &string}
#[derive(FromValue)]
#[value(shape = "graphix_compiler::rep::GraphixShape")]
struct Point {
    x: f64,
    y: f64,
    label: BindId,
}
}
#![allow(unused)]
fn main() {
// assume we got id from a data structure and its type is &i64
let mut r = handle.compile_ref(id).await?;

// if the variable r is bound to has a value right now it will be in last
if let Some(v) = &r.last {
    println!("current value: {v}")
}

// now we will get an update whenever the variable updates
let mut batch = rx.recv().await.ok_or_else(|| anyhow::anyhow!("the runtime is dead"))?;
for ev in batch.drain(..) {
    match ev {
        graphix_rt::GXEvent::Updated(id, v) => {
            if let Some(v) = r.update(id, &v) {
                println!("current value {v}")
            }
        },
        graphix_rt::GXEvent::Env(_) => ()
    }
}
}

You can also set refs, which is exactly the same thing as the connect operator <-, and does what you expect it should do.

Ref By Name

We can also reference a variable by name:

#![allow(unused)]
fn main() {
let mut r = handle.compile_ref_by_name(
    &env,
    &graphix_compiler::Scope::root(),
    &graphix_compiler::ModPath::from(["foo"]),
).await?;

// the rest of the code is exactly the same
}

Calling Graphix Functions

A function is called through a call site the runtime compiles for it, a Callable. compile_callable takes the function as a Value: one an expression returned, or a variable’s value. To call a function by name, take its value through a ref first:

#![allow(unused)]
fn main() {
let r = handle.compile_ref_by_name(
    &env,
    &graphix_compiler::Scope::root(),
    &graphix_compiler::ModPath::from(["sum"]),
).await?;
let sum = r.last.clone().ok_or_else(|| anyhow::anyhow!("sum has no value yet"))?;
let f = handle.compile_callable(sum).await?;
}

call_answered calls it, and its return value arrives on the channel under the callable’s answer id:

#![allow(unused)]
fn main() {
f.call_answered(netidx_value::ValArray::from_iter_exact(
    [Value::from(1), Value::from(2), Value::from(3)]
))?;
'answer: loop {
    let mut batch = rx.recv().await.ok_or_else(|| anyhow::anyhow!("the runtime is dead"))?;
    for ev in batch.drain(..) {
        match ev {
            graphix_rt::GXEvent::Updated(id, v) if id == f.answer => {
                println!("sum returned {v}");
                break 'answer;
            }
            graphix_rt::GXEvent::Env(e) => env = e,
            _ => (),
        }
    }
}
}

call makes the same call without an answer, for a function called for its effects (an event handler).