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, } } }