Skip to content

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:

Rust
dusk_program_proc::metadata!("sleep", VERSION, sleep_capnp::PROGRAM_ID);

1. Program id

A u64 constant identifying the program, declared in its .capnp file:

Cap'n Proto
const programId :UInt64 = 0xc0da1e11ed8d4a36;

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):

Cap'n Proto
struct SleepArgs {
  struct Data { durationMs @0 :UInt64; }
  interface Server {}
}

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:

Rust
#[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:

Rust
#[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:

Cap'n Proto
interface SleepPortal extends(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.