Architecture

NockApp is a framework, which means that it's a collection of libraries and tools which work together to produce

The basic pattern for a NockApp application is for the runtime to supply a NockVM, a set of I/O drivers, and a boot sequence. Applications can assume either one-shot/batch or main loop/continuous forms based on their anticipated use pattern.

The point of a NockApp application is to provide necessary scaffolding for a Nock ISA kernel to receive and process events, resulting in a pair of a list of emitted effects and a new kernel state. These are automatically persisted so that subsequent calls even to a one-shot application know about previous state.

Pokes issued from the runtime must match the expected command format for the kernel. By convention, we call this a $cause. For instance, here is the Rust driver-side construction of a poke [%cause ~]:

let mut poke_slab = NounSlab::new();
let command_noun = T(&mut poke_slab, &[D(tas!(b"cause")), D(0x0)]);
poke_slab.set_root(command_noun);

The corresponding Hoon $cause type looks like this:

+$  cause
  $%  [%cause ~]
  ==

In this trivial case, there is only one cause available, but obviously most of the time we want to have more options available. We'll see those demonstrated later.

The objective for NockApp development is for developers to need to write only a minimum of Rust runtime code, spending most of their time on Nock ISA kernel logic in Hoon (or Jock, once the beta is released).

One-Shot/Batch Applications

A one-shot application has a Rust wrapper to set up a single poke which is then evaluated. This is useful for single-interaction instances, such as the current wallet pattern. A specific set of drivers and a poke are constructed and computed, then emitted effects are processed by the drivers.

In fact, a one-shot application may not need an I/O driver at all. The basic template in Nockup illustrates how to construct a poke manually, inject it as a command, and iterate over emitted effects.

Basic Example

For instance, here is the Rust driver for a simple poke-and-return kernel:

use std::error::Error;
use std::fs;

use nockapp::kernel::boot;
use nockapp::{exit_driver, http_driver, AtomExt, NockApp};

use nockapp::noun::slab::NounSlab;
use nockapp::wire::{SystemWire, Wire};
use nockvm::noun::{Atom, D, T};
use nockvm_macros::tas;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let cli = boot::default_boot_cli(false);
    boot::init_default_tracing(&cli);
    
    let kernel = fs::read("out.jam")
        .map_err(|e| format!("Failed to read out.jam: {}", e))?;

    let mut nockapp: NockApp = boot::setup(&kernel, Some(cli), &[], "{{project_name}}", None).await?;

    let mut poke_slab = NounSlab::new();
    let command_noun = T(&mut poke_slab, &[D(tas!(b"cause")), D(0x0)]);
    poke_slab.set_root(command_noun);

    // Poke and process each emitted effect.
    let result = match nockapp.poke(SystemWire.to_wire(), poke_slab).await {
        Ok(effects) => {
            let mut results = Vec::new();
            for (_i, effect) in effects.iter().enumerate() {
                let effect_noun = unsafe { effect.root() };
                if let Ok(cell) = effect_noun.as_cell() {
                    let Ok(tail_atom) = cell.tail().as_atom() else { 
                        continue; 
                    };
                    let Ok(tail_string) = std::str::from_utf8(tail_atom.as_ne_bytes()) else {
                        continue;
                    };
                    results.push(tail_string.trim_end_matches('\0').to_string());
                }
            }
            results.last().unwrap_or(&String::new()).clone()
        }
        Err(_e) => {
            "command failed".to_string()
        }
    };

    println!("{}", result);
    Ok(())
}

The corresponding Hoon looks like this:

/+  lib
/=  *  /common/wrapper
::
=>
|%
+$  versioned-state
  $:  %v1
      ~
  ==
::
+$  effect
  $%  [%effect @t]
  ==
::
+$  cause
  $%  [%cause ~]
  ==
--
|%
++  moat  (keep versioned-state)
::
++  inner
  |_  state=versioned-state
  ::
  ++  load
    |=  old-state=versioned-state
    ^-  _state
    ?:  =(-.old-state %v1)
      old-state
    old-state
  ::
  ++  peek
    |=  =path
    ^-  (unit (unit *))
    ~>  %slog.[0 'Peeks awaiting implementation']
    ~
  ::
  ++  poke
    |=  =ovum:moat
    ^-  [(list effect) _state]
    =/  cause  ((soft cause) cause.input.ovum)
    ?~  cause
      ~>  %slog.[3 (crip "invalid cause {<cause.input.ovum>}")]
      :_  state
      ^-  (list effect)
      ~[[%effect 'Invalid cause format']]
    ~>  %slog.[1 (cat 3 'poked: ' -.u.cause)]
    ~>  %slog.[0 'Pokes awaiting implementation']
    [~ state]
  --
--
((moat |) inner)

(The entire example is available in Nockup as the basic template.)

The meat of the Hoon-side logic is in the arms:

  • +load simply verifies that the state has a version head tag of %v1. There is, in this example, no upgrade or migration logic.

  • +peek displays a side effect output string (a "slog", or "side log") and returns an empty value, ~ or 0x0.

  • +poke unwraps the cause safely (soft), verifies that it unwrapped (i.e. it fit the [%cause ~] pattern), and prints the basic poke data as a slog. It then returns an empty list of effects and the current state with no changes. (There's more to say here, but the return type marked with ^- is a list of emitted effects and a state matching the shape—not necessarily the values—of the current state.)

To write a batch application, you need to alter the Hoon $cause and $effect data types and provide appropriate business logic. On the Rust driver side, you need to supply a poke which has the proper format matching one or more Hoon $causes. This may be processed from CLI arguments or other sources.

Main Loop/Continuous Applications

A continuous application feels more like a server: it remains live, responds to incoming data, and emits effects from time to time. The process stays live and the Rust runtime continues to inject $causes into the Nock ISA kernel as events arrive (keystrokes, network communications, etc.).

gRPC Server Example

The Rust driver for a gRPC broadcaster looks like the following. This will automatically connect to a gRPC server (by default at 5555) and submit events to it with an associated PID and target path, interpretable by the recipient.

use std::error::Error;
use std::fs;
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::Path;

use nockapp::driver::{make_driver, IODriverFn, NockAppHandle, Operation};
use nockapp::kernel::boot;
use nockapp::noun::slab::NounSlab;
use nockapp::wire::{SystemWire, Wire, WireRepr, WireTag as AppWireTag};
use nockapp::{AtomExt, Bytes, NockApp, NockAppError, Noun};
use nockapp::{exit_driver, file_driver};
use nockapp::utils::make_tas;
use nockapp_grpc::NockAppGrpcServer;
use nockapp_grpc::client::NockAppGrpcClient;
use nockapp_grpc::driver::{GrpcEffect, grpc_listener_driver, grpc_server_driver};
use nockapp_grpc::wire_conversion::{create_grpc_wire, grpc_wire_to_nockapp};
use nockvm::noun::{Atom, D, T};
use nockvm_macros::tas;
use noun_serde::{NounDecode, NounDecodeError, NounEncode};
use tracing::{error, info};

use grpc::string_to_atom;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let cli = boot::default_boot_cli(false);
    boot::init_default_tracing(&cli);

    let source_filename = Path::new(file!())
        .file_stem()
        .unwrap()
        .to_str()
        .unwrap();
    let fallback_filename = format!("{}.jam", source_filename);

    let kernel = fs::read("out.jam")
        .or_else(|_| fs::read(&fallback_filename))
        .map_err(|e| format!("Failed to read kernel file: {}", e))?;
    let mut nockapp: NockApp = boot::setup(
        &kernel,
        Some(cli),
        &[],
        source_filename,
        None
    )
    .await
    .map_err(|e| format!("Kernel setup failed: {}", e))?;

    //  Load demo poke.
    let mut poke_slab = NounSlab::new();
    let str_atom = string_to_atom(&mut poke_slab, "hello world")?;
    let head = make_tas(&mut poke_slab, "poke-value").as_noun();
    let command_noun = T(&mut poke_slab, &[head, str_atom.as_noun()]);
    poke_slab.set_root(command_noun);

    //  The demo poke generates a %grpc effect which we want to emit.
    nockapp
        .add_io_driver(nockapp::one_punch_driver(poke_slab, Operation::Poke))
        .await;
    nockapp
        .add_io_driver(grpc_listener_driver(format!("http://127.0.0.1:{}", grpc::GRPC_PORT.to_string())))
        .await;
    nockapp
        .add_io_driver(exit_driver())
        .await;

    nockapp.run().await;

    Ok(())
}

The basic pattern is to add the necessary drivers, construct the boot poke, and run the kernel. (In this case, we don't have a trigger for subsequent calls to the kernel, but they are straightforward to add and other applications show them off.)

The Hoon kernel corresponding to it emits gRPC messages as a list of effects and then stays live:

/+  *lib
/=  *  /common/wrapper
::
=>
|%
+$  versioned-state
  $:  %v1
      ~
  ==
::
+$  cause
  $%  [%cause ~]
      [%command val=@t]
      ^cause
  ==
::
+$  effect
  $%  [%effect msg=@]
      ^effect
  ==
--
|%
++  moat  (keep versioned-state)
::
++  inner
  |_  state=versioned-state
  ::
  ++  load
    |=  old-state=versioned-state
    ^-  _state
    ?:  =(-.old-state %v1)
      old-state
    old-state
  ::
  ++  peek
    |=  =path
    ^-  (unit (unit *))
    ~>  %slog.[0 'Peeks awaiting implementation']
    ~
  ::
  ++  poke
    |=  =ovum:moat
    ^-  [(list effect) _state]
    =/  cause  ((soft cause) cause.input.ovum)
    ?~  cause
      ~>  %slog.[3 (crip "invalid cause {<cause.input.ovum>}")]
      :_  state
      ^-  (list effect)
      ~[[%effect 'Invalid cause format']]
    :_  state
    ^-  (list effect)
    =/  pid  42  :: implementation-specific meaning
    =/  val  -.u.cause
    :~  [%grpc %peek pid %talk /path]
        [%grpc %poke pid val]
        :: [%exit ~]  :: uncomment this to exit after emitting the effects
    ==
  --
--
((moat |) inner)

(This example is available with Nockup as grpc.)

To reiterate, most of the work by a developer will be to select an apt base main.rs template, modify the $causes in both Rust and Hoon, and handle effects appropriately.

Runtime Drivers

A number of runtime I/O drivers are available with NockApp out of the box:

  • grpc_listener_driver

  • grpc_server_driver

  • exit_driver

  • fakenet_driver

  • file_driver

  • http_driver

  • libp2p_driver

  • markdown_driver

  • mining_driver

  • one_punch_driver

  • poke_once_driver

  • timer_driver

Over time, more drivers will be added to this list. We also envision a plugin system to make drivers easier to use without have to work with the Rust wrapper directly.

Last updated