Custom Embedded Applications
For most standalone binaries, graphix package build-standalone is the simplest
approach — see Standalone Binaries. This section
covers the more advanced case where you need full control: custom module
resolvers, embedded REPLs, compiler flags, or integration with your own Rust
application.
Using the
graphix-shell crate you
can build a custom Graphix application. A ShellBuilder starts with
the standard library’s packages (graphix_shell::stdlib_packages());
add your own with add_packages, e.g.
.add_packages(graphix_package::packages!()) for your graphix-package-*
dependencies, or replace the set with packages(..).
Basic Application
The shell needs a MainThreadHandle so widgets that must run on the
main thread (notably the GUI backend) can dispatch work back from the
tokio runtime. The standard pattern is to spawn tokio on its own thread
and pump the main-thread queue from main:
use anyhow::Result;
use graphix_compiler::expr::Load;
use graphix_package::MainThreadHandle;
use graphix_rt::NoExt;
use graphix_shell::{Mode, ShellBuilder};
use std::path::PathBuf;
// the exit code a script asked for with `sys::exit`, if any
async fn tokio_main(run_on_main: MainThreadHandle) -> Result<Option<i32>> {
ShellBuilder::<NoExt>::default()
.mode(Mode::Script(Load::File(PathBuf::from("main.gx"))))
.no_init(true)
.build()?
.run(run_on_main)
.await
}
fn main() -> Result<()> {
let (handle, main_rx) = MainThreadHandle::new();
let tokio_thread = std::thread::Builder::new()
.name("tokio".into())
.spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(tokio_main(handle))
})
.expect("spawn tokio thread");
while let Ok(f) = main_rx.recv() {
f();
}
if let Some(code) = tokio_thread.join().expect("tokio thread panicked")? {
std::process::exit(code)
}
Ok(())
}
MainThreadHandle::new() returns the handle you pass to .run() and a
receiver you drive on the main thread. The receiver yields closures the
shell wants executed there; calling each in turn is enough.
Netidx Configuration
By default sys::net runs against a process-internal netidx (resolver,
publisher, and subscriber in-process), materialized lazily the first
time a program performs a netidx operation. Nothing to configure — and
programs that never touch sys::net never create it.
To connect to a real netidx environment, seed a
NetConfig
into the context with setup_context — the general hook for
embedder-seeded package state, run against the freshly created context
before anything compiles:
#![allow(unused)]
fn main() {
use graphix_package_core::NetConfig;
use netidx::publisher::DesiredAuth;
let config = netidx::config::Config::load_default()?;
ShellBuilder::<NoExt>::default()
.mode(Mode::Script(Load::File(PathBuf::from("main.gx"))))
.setup_context(Box::new(move |ctx| {
ctx.libstate.set(NetConfig::Config {
config,
auth: DesiredAuth::Anonymous,
bind: None,
});
}))
.build()?
.run(run_on_main)
.await
}
Use NetConfig::Ready { publisher, subscriber } if your application
already holds netidx handles it wants graphix to share. Other packages
document their own libstate entries; setup_context is the single
place to seed them all.
Module Resolvers
If you want to bundle additional Graphix source files into your binary (beyond what packages provide), you can add module resolvers. A VFS resolver maps virtual paths to source code:
#![allow(unused)]
fn main() {
use ahash::AHashMap;
use arcstr::literal;
use graphix_compiler::expr::{ResolverRef, VfsResolver};
use netidx_core::path::Path;
fn my_modules() -> ResolverRef {
VfsResolver::new(AHashMap::from_iter([
(Path::from("/myapp/mod.gx"), literal!(include_str!("myapp/mod.gx")).into()),
(Path::from("/myapp/util.gx"), literal!(include_str!("myapp/util.gx")).into()),
]))
}
ShellBuilder::<NoExt>::default()
.module_resolvers(vec![my_modules()])
.mode(Mode::Script(Load::File(PathBuf::from("main.gx"))))
.build()?
.run(run_on_main)
.await
}
Module loading is the
ModuleResolver
trait, so a resolver can load source from anywhere — the sys package
ships one that loads modules published in netidx (the shell’s
netidx: scheme).
A VFS key is a file path as the resolver looks it up: module m is
/m.gx or /m/mod.gx, and m::n is /m/n.gx or /m/n/mod.gx. You
can have as many module resolvers as you like. When loading modules they are
checked in order, so earlier ones shadow later ones.
Note that for most cases, creating a package is
preferable to manually constructing VFS resolvers. Packages handle module
registration automatically through the defpackage! macro.
Custom REPL
You can build a REPL with pre-loaded modules by setting the mode to
Mode::Repl:
#![allow(unused)]
fn main() {
ShellBuilder::<NoExt>::default()
.module_resolvers(vec![my_modules()])
.mode(Mode::Repl)
.build()?
.run(run_on_main)
.await
}
This gives you a REPL with the builder’s packages and your additional modules available.
Compiler Flags
You can enable or disable compiler flags:
#![allow(unused)]
fn main() {
use graphix_compiler::CFlag;
ShellBuilder::<NoExt>::default()
.enable_flags(CFlag::WarnUnhandled | CFlag::WarningsAreErrors)
.mode(Mode::Repl)
// ...
}