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 run time.

Tokio

graphix-rt uses tokio internally, it will run the compiler and run loop in it's 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 actually 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() {
// set up an execution context using the generic runtime with no customization
let mut ctx = ExecCtx::new(GXRt::<NoExt>::new(publisher, subscriber));
// ... 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 load
    // it as part of the root module. Otherwise you will get the bare compiler.
    .root(literal!("root.gx"))
    .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(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 it's id on the channel.
// This is a batch of GXEvents
let mut batch = rx.recv().await.ok_or_else(|| anyhow!("the runtime is dead"))?;
let mut env = handle.get_env().await?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) if id == e.id => println!("2 + 2 = {v}"),
        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.

#![allow(unused)]
fn main() {
// assume we got id from a data structure and it's type is &i64
let mut r = handle.compile_ref(id)

// 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!("the runtime is dead"))?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) => {
            if let Some(v) = r.update(id, &v) {
                println!("current value {v}")
            }
        },
        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, &Scope::root(), &ModPath::from(["foo"])).await?;

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

Calling Graphix Functions

Now lets register a call site, call a Graphix function, and get it's result. We do this by calling compile_callable_by_name on the handle.

#![allow(unused)]
fn main() {
let mut f = handle.compile_callable_by_name(&env, &Scope::root(), &ModPath::from(["sum"])).await?;
f.call(ValArray::from_iter_exact([Value::from(1), Value::from(2), Value::from(3)])).await?;

// now we must update f to drive both late binding, and get our return value
// we need a loop this time because there will be multiple updates
loop {
    let mut batch = rx.recv().ok_or_else(|| anyhow!("the runtime is dead"))?;
    for ev in batch.drain(..) {
        match ev {
            GXEvent::Updated(id, v) => {
                if let Some(v) = f.update(&v).await {
                    println!("sum returned {v}")
                }
            }
            GXEvent::Env(e) => env = e,
        }
    }
}
}

Calling Functions by LambdaId

The above case applies when we only know the name of the function we want to call, which is less common than you might imagine. If the function was passed in to us, for example we evaluated an expression that returned a function, then it's actually easier to deal with because we don't have to handle late binding. In this case we can call compile_callable on the handle.

#![allow(unused)]
fn main() {
// id is the LambdaId of the function as a Value. Lets assume it's sum
let f = handle.compile_callable(id).await?;
f.call(ValArray::from_iter_exact([Value::from(1), Value::from(41)])).await?;

// now wait for the value
let mut batch = rx.recv().ok_or_else(|| anyhow!("the runtime is dead"))?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) => {
            if let Some(v) = f.update(&v) {
                println!("sum returned {v}")
            }
        }
        GxEvent::Env(e) => env = e,
    }
}
}