Sending and Responding to a Message

In this section you will learn how to use different parts of a process, how Request-Response handling works, and other implementation details with regards to messaging. The process you will build will be simple — it messages itself and responds to itself, printing whenever it gets messages.

Note — the app you will build in Sections 2 through 5 is not my-chat-app; it is simply a series of examples designed to demonstrate how to use the system's features.

Requirements

This section assumes you've completed the steps outlined in Environment Setup to setup your development environment or otherwise have a basic Kinode app open in your code editor of choice. You should also be actively running a Kinode (live or fake) such that you can quickly compile and test your code! Tight feedback loops when building: very important.

Starting from Scratch

If you want to hit the ground running by yourself, you can take the template code or the chess tutorial and start hacking away. Here, you'll start from scratch and learn about every line of boilerplate. Open src/lib.rs, clear its contents so it's empty, and code along!

The last section explained packages, the package manifest, and metadata. Every package contains one or more processes, which are the actual Wasm programs that will run on a node.

The Generating WIT Bindings and init() Function subsections explain the boilerplate code in detail, so if you just want to run some code, you can skip to Running First Bits of Code.

Generating WIT Bindings

For the purposes of this tutorial, crucial information from this Wasm documentation has been abridged in this small subsection.

A Wasm component is a wrapper around a core module that specifies its imports and exports. E.g. a Go component can communicate directly and safely with a C or Rust component. It need not even know what language another component was written in — it needs only the component interface, expressed in WIT.

The external interface of a component — its imports and exports — is described by a world. Exports are provided by the component, and define what consumers of the component may call; imports are things the component may call. The component, however, internally defines how that world is implemented. This interface is defined via WIT.

WIT bindings are the glue code that is necessary for the interaction between Wasm modules and their host environment. They may be written in any Wasm-compatible language — Kinode offers the most support for Rust with kit and process_lib. The world, types, imports, and exports are all declared in a WIT file, and using that file, wit_bindgen generates the code for the bindings.

So, to bring it all together...

Every process must generate WIT bindings based on a WIT file for either the default process-v0 world or a package-specific world in order to interface with the Kinode kernel:

#![allow(unused)]
fn main() {
wit_bindgen::generate!({
    path: "target/wit",
    world: "process-v0",
});
}

init() Function

The entrypoint of a process is the init() function. You can use the call_init!() macro as shown below. Skip to the code to see how to do so. The following prose is an explanation of what is happening under-the-hood in the call_init() macro.

After generating the bindings, every process must define a Component struct which implements the Guest trait (i.e. a wrapper around the process which defines the export interface, as discussed above). The Guest trait should define a single function — init(). This is the entry point for the process, and the init() function is the first function called by the Kinode runtime when the process is started.

The definition of the Component struct can be done manually, but it's easier to import the kinode_process_lib crate (a sort of standard library for Kinode processes written in Rust) and use the call_init! macro.

#![allow(unused)]
fn main() {
use kinode_process_lib::{await_message, call_init, println, Address, Request, Response};

wit_bindgen::generate!({
    path: "target/wit",
    world: "process-v0",
});

call_init!(init);
fn init(our: Address) {
...
}

Every Kinode process written in Rust will need code that does the same thing as the code above (i.e. use the wit_bindgen::generate!() and call_init!() macros). See kinode.wit for more details on what is imported by the WIT bindgen macro. These imports are the necessary "system calls" for talking to other processes and runtime components on Kinode. Note that there are a variety of imports from the process_lib including a println! macro that replaces the standard Rust one.

The our parameter tells the process what its globally-unique name is.

The init() function can either do one task and then return, or it can loop, waiting for a new message and then acting based on the nature of the message. The first pattern is more usual for scripts that do one task and then exit. The second pattern is more usual for long-lived state machine processes that, e.g., serve data over the network or over HTTP.

Sending a Message

The Request type from the process_lib provides all the necessary functionality to send a Message.

Request is a builder struct that abstracts over the raw interface presented in the WIT bindings. It's very simple to use:

#![allow(unused)]
fn main() {
    Request::to(my_target_address)
        .body(my_body_bytes)
        .send();
}

Because this process might not have capabilities to message any other (local or remote) processes, for the purposes of this tutorial, just send the message to itself.

#![allow(unused)]
fn main() {
    Request::to(&our)
        .body(b"hello world")
        .send();
}

Note that send() returns a Result. If you know that a target and body was set, you can safely unwrap this: send will only fail if one of those two fields are missing.

You can modify your Request to expect a Response, and your message-handling to send one back, as well as parse the received Request into a string.

#![allow(unused)]
fn main() {
    Request::to(&our)
        .body(b"hello world")
        .expects_response(5)
        .send()
        .unwrap();
}

The expects_response method takes a timeout in seconds. If the timeout is reached, the Request will be returned to the process that sent it as an error. If you add that to the code above, you'll see the error after 5 seconds in your node's terminal.

Responding to a Message

Now, consider how to handle the Request. The await_message() function returns a Result that looks like this:

#![allow(unused)]
fn main() {
Result<Message, SendError>
}

The SendError is returned when a Request times out or, if the Request passes over the network, in case of a networking issue. Use a match statement to check whether the incoming value is a message or an error, then branch on whether the message is a Request or a Response. To send a Response back, import the Response type from process_lib and send one from the Request branch.

#![allow(unused)]
fn main() {
    loop {
        match await_message() {
            Err(send_error) => println!("got SendError: {send_error}"),
            Ok(message) => {
                let body = String::from_utf8_lossy(message.body());
                if message.is_request() {
                    println!("got a request: {body}");
                    Response::new()
                        .body(b"hello world to you too!")
                        .send()
                        .unwrap();
                } else {
                    println!("got a response: {body}");
                }
            }
        }
    }
}

Putting it all together:

#![allow(unused)]
fn main() {
use kinode_process_lib::{await_message, call_init, println, Address, Request, Response};

wit_bindgen::generate!({
    path: "target/wit",
    world: "process-v0",
});

call_init!(init);
fn init(our: Address) {
    println!("begin");

    Request::to(&our)
        .body(b"hello world")
        .expects_response(5)
        .send()
        .unwrap();

    loop {
        match await_message() {
            Err(send_error) => println!("got SendError: {send_error}"),
            Ok(message) => {
                let body = String::from_utf8_lossy(message.body());
                if message.is_request() {
                    println!("got a request: {body}");
                    Response::new()
                        .body(b"hello world to you too!")
                        .send()
                        .unwrap();
                } else {
                    println!("got a response: {body}");
                }
            }
        }
    }
}
}

Run

kit build your_pkg_directory
kit start-package your_pkg_directory -p 8080

to see the messages being sent by your process.

You can find the full code here.

The basic structure of this process — an infinite loop of await_message() and then handling logic — can be found in the majority of Kinode processes. The other common structure is a script-like process, that handles and sends a fixed series of messages and then exits.

In the next section, you will learn how to turn this very basic Request-Response` pattern into something that can be extensible and composable.

Get Help: