Programs
Programs are part of the framework layer - the advanced way to use Dusk. Most integrations embed a node and run the built-in programs without ever writing one. Reach for this when you're customizing Dusk.
A program is a unit of work a node can run - something like ps, or one of
your own. Programs are static: each is compiled into a node (there is no dynamic
loading), and a client turns one into a running process by
calling Dusk.process.
Every program is made of five parts, plus a one-line metadata! declaration that
ties them together:
1. Program id
A u64 constant identifying the program, declared in its .capnp file:
It is the dispatch key: a node's LauncherSet matches an incoming Dusk.process
to the launcher with the same id.
2. Args
A program's args are a capnp struct with two nested members - startup Data and
a Server interface of client-side callbacks (often empty):
These supply the two type parameters of the core ProgramArgs(D, S) wire type.
On the Rust side you declare an Args struct with a single #[data] field and
host its Server:
#[derive(dusk_program_proc::Args)]
pub struct Args {
#[data]
pub data: ArgsDataBuilder,
}
#[dusk_program_proc::impl_args_rpc_server]
impl Args {}
#[derive(Args)] packs the data and the Server capability into a
ProgramArgs; #[impl_args_rpc_server] hosts the Server interface so the node
can call those callbacks back on the client.
The Server is what serializing drops
A capability is a live object on a connection, and bytes cannot carry one. So
when a command's args are serialized to bytes - compile_sh! does it through
compile_to_words to compile a command into a binary, and the same holds for
bytes written to a file - their Data is kept and their Server is dropped. A
program launched from those args finds no Server: program_args.server_as()
fails with Message contains null capability pointer. Bytecode sent to a node
over a connection is not serialized this way, and each of its commands keeps its
Server.
The Server can also be there and dead. It is hosted by the client that built
the args, so once that client disconnects, every call to it fails - with
Disconnected, or Premature end of file if the connection died in the middle
of a message. That is what a command in a function defined over another
connection, or in a detached script, meets after the client that typed it has
gone.
Either way the program is running where no client is listening. A program that
calls its Server should, where it can, fall back to working without it when
the Server is missing or disconnected, and return every other error.
programs does: its Server names each program from the client's shell entries,
and without it the node sends the list without names.
A capability belongs in the Server, never in Data. Serializing refuses a
command whose Data holds one - compile_sh! fails with an error naming the
program - so a program that keeps a capability in its Data can never run from
serialized args. ArgsDataBuilder has no capability table either, so setting a
capability in it panics before it gets that far. sh is the one exception: its
Data is itself bytecode, and serializing rebuilds it command by command, dropping
each command's Server.
3. Launcher
The factory that builds a process from args. Derive its identity and write the one build method:
#[derive(dusk_program_proc::Launcher)]
pub struct Launcher;
#[async_trait::async_trait(?Send)]
impl dusk_program::launcher::LauncherMixin for Launcher {
async fn launch(&mut self, process_context: ProcessContext)
-> anyhow::Result<Box<dyn Process>> {
Ok(Box::new(Process::with_context(process_context).await?))
}
}
See Launchers.
4. Process
The async runtime: a struct carrying a #[process_context] field, with
#[derive(Process)] for the identity methods and a ProcessMixin impl providing
with_context, main, and portal. See Processes.
5. Portal
The typed capability clients call - a capnp interface extending Dusk.Portal:
On the Rust side, #[derive(Portal)] serves the base programId method and
#[impl_portal_rpc_server] hosts the program-specific interface. See
Portals & Streams.
Making a program shell-invocable
The five parts make a program runnable over Dusk.process. To also run it from
the shell by name, give its client side an
#[sh_entry] function that registers the shell entry name and its help text. That
registration is part of the shell, not of the core program model.