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

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. All installed packages are automatically registered, so your application gets the full standard library and any additional packages out of the box.

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::Source;
use graphix_rt::NoExt;
use graphix_shell::{MainThreadHandle, Mode, ShellBuilder};

async fn tokio_main(run_on_main: MainThreadHandle) -> Result<()> {
    ShellBuilder::<NoExt>::default()
        .mode(Mode::Script(Source::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();
    }
    tokio_thread.join().expect("tokio thread panicked")
}

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(Source::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"), literal!(include_str!("myapp/mod.gx")).into()),
        (Path::from("/myapp/util"), literal!(include_str!("myapp/util.gx")).into()),
    ]))
}

ShellBuilder::<NoExt>::default()
    .module_resolvers(vec![my_modules()])
    .mode(Mode::Script(Source::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).

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 standard library, all installed 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::WarnUnused | CFlag::WarnUnhandled)
    .mode(Mode::Repl)
    // ...
}