Skip to content

5. Packaging as a Component

So far you have built every Process, Container and ContainerProcess as a separate file (library), one per type. In the configuration file in chapter 3, you also listed the types in plugins one by one so each file was loaded.

When you want to hand someone “the full set of parts for this camera”, scattered files are inconvenient. In this chapter you build a Component that bundles Processes, Containers and ContainerProcesses into one library, and load it from a configuration file.

Keep using the example repository you cloned and built in chapter 1. The Component example was built along with everything else in chapter 1. If you have not done that yet, follow Prepare the samples in chapter 1.

The Component used in this chapter contains the following. Each works the same as what you built in the earlier chapters.

Kind What it does
Process Adds 1 to its argument and returns it (same as increment_process in chapter 1)
Container Holds one integer (same as the counter in chapter 2)
ContainerProcess Reads or changes the Container’s value

component/example_component/src/lib.rs

use juiz_sdk::prelude::*;
#[juiz_component_process]
fn example_component_increment(arg1: i64) -> JuizResult<Capsule> {
log::trace!("increment_process({:?}) called", arg1);
return Ok(jvalue!(arg1 + 1).into());
}
#[repr(Rust)]
pub struct ExampleComponentContainer {
pub value: i64,
}
#[juiz_component_container]
fn example_component_container(initial_value: i64) -> JuizResult<Box<ExampleComponentContainer>> {
println!("example_component_container({initial_value}) called");
Ok(Box::new(ExampleComponentContainer {
value: initial_value,
}))
}
#[juiz_component_container_process(container_type = "example_component_container")]
fn example_component_container_get(
container: &mut ContainerImpl<ExampleComponentContainer>,
) -> JuizResult<Capsule> {
println!("example_component_container_get()");
Ok(jvalue!(container.value).into())
}
#[juiz_component_container_process(container_type = "example_component_container")]
fn example_component_container_increment(
container: &mut ContainerImpl<ExampleComponentContainer>,
) -> JuizResult<Capsule> {
println!("example_component_container_increment()");
container.value = container.value + 1;
Ok(jvalue!(container.value).into())
}
#[juiz_component_container_process( container_type = "example_component_container"
arguments = {
default = {
arg1 = 1
}
}
)]
fn example_component_container_add(
container: &mut ContainerImpl<ExampleComponentContainer>,
arg1: i64,
) -> JuizResult<Capsule> {
println!("example_component_container_add({arg1})");
container.value = container.value + arg1;
Ok(jvalue!(container.value).into())
}
juiz_component_manifest!(
component_name = "example_component"
containers = {
example_component_container = [
example_component_container_get,
example_component_container_increment,
example_component_container_add
]
}
processes = [
example_component_increment
]
);

The functions are written almost the same way as before. Only two things differ.

  • The attribute macros change to their component versions: #[juiz_process] → #[juiz_component_process], #[juiz_container] → #[juiz_component_container], and #[juiz_container_process] → #[juiz_component_container_process].
  • The final juiz_component_manifest! declares the Component’s name and the Processes, Containers and ContainerProcesses it contains. ContainerProcesses are listed under the Container they are bound to.

The whole Component is one crate (crate-type = ["cdylib"] in Cargo.toml), built into a single shared library, target/debug/libexample_component.so (.dylib on macOS).

What used to be split per type now lives in one library (one file in Python). The bodies of the Processes and ContainerProcesses are unchanged. Processes are still idempotent functions, and only ContainerProcesses touch state.

example_component.conf

name: example_component
option:
http_broker:
start: true
port: 8000
plugins:
components:
example_component:
language: rust
path: "./target/debug"
processes:
- type_name: example_component_increment
name: proc0
containers:
- type_name: example_component_container
name: container0
initial_value: 100
processes:
- type_name: example_component_container_increment
name: increment0
- type_name: example_component_container_add
name: add0
- type_name: example_component_container_get
name: get0

Compare it with the configuration file from chapter 3.

plugins:
components:
example_component:
path: "./target/debug"

In chapter 3 you listed types one by one in process_factories and container_factories, and each ContainerProcess under its Container. With a Component, you write just one Component name in components. As in chapter 3, the key determines which file is loaded.

For the Component name example_component, juiz loads libexample_component.so from ./target/debug.

That one entry loads every Process, Container and ContainerProcess type in the Component.

processes and containers: created as usual

Section titled “processes and containers: created as usual”

processes and containers are written exactly as in chapter 3. Put a type name from the Component in type_name and the instance name in name. You can pass a Container’s initial value the same way, too (the Python example omits it, so the value starts at 0).

Where a type came from does not matter to the side creating instances. A Component is for “distributing types together”, and the instances you create behave exactly as if they had been loaded from separate files.

Pass the configuration file with -f. The command is the same for every language.

Terminal window
juiz -f example_component.conf -d

For Python, run it in a terminal where the venv from chapter 1 is activated.

Leave juiz running, open another terminal, and look at the lists.

Terminal window
juiz process list
# => ["http://127.0.0.1:8000/process/proc0::example_component_increment"]
juiz container list
# => ["core://core/container/container0::example_component_container"]
juiz container-process list
# => ["core://core/container_process/add0::example_component_container_add:container0", "core://core/container_process/get0::example_component_container_get:container0", "core://core/container_process/increment0::example_component_container_increment:container0"]

Process, Container and ContainerProcess instances have all been created from types loaded from one file. The IDs look exactly like those in earlier chapters. Call each of them.

Terminal window
juiz process call "http://127.0.0.1:8000/process/proc0::example_component_increment" '{"arg1": 3}' -p
# => CapsulePtr((value:Value(4), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/get0::example_component_container_get:container0" '{}' -p
# => CapsulePtr((value:Value(100), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/increment0::example_component_container_increment:container0" '{}' -p
# => CapsulePtr((value:Value(101), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/add0::example_component_container_add:container0" '{"arg1": 5}' -p
# => CapsulePtr((value:Value(106), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/get0::example_component_container_get:container0" '{}' -p
# => CapsulePtr((value:Value(106), option: []))

The Process adds 1 to its argument and returns it, and the ContainerProcesses change or read the Container’s value. From the caller’s point of view, they are indistinguishable from what you built from separate files in chapters 1 to 3.

As you have seen, bundling into a Component changes nothing about how Processes and Containers work or how you use them. Only how the files are bundled changes.

  • Authors can write related Processes, Containers and ContainerProcesses in one project and distribute them as one library. For a camera, for example, a Container that opens and holds the camera, a ContainerProcess that reads images, and a Process that converts images can all be one Component.
  • Users write one line in components under plugins to get every type inside. Which types to instantiate, and how many, is still decided freely with processes and containers.

In other words, Processes, Containers and ContainerProcesses are “units for running”, while a Component is the “unit for distributing”.

  • Add another instance of example_component_increment (example_component_python_increment in Python, example_component_cpp_increment in C++) to processes. As in chapter 3, you can create any number of instances from types loaded from a Component.
  • Add another Container (for example container1) and its ContainerProcesses to containers. container0 and container1 hold separate values.
  • Add connections and ecs from chapter 4 and connect the first Process’s output to arg1 of the second Process you added. Types from a Component connect the same way as any other. plugins can list components and process_factories side by side, so you can also connect to a Process from a separate file, such as increment_process from chapter 3.
  • You can also load a Component from the command line without a configuration file. With -1, juiz creates one instance of every type in the Component (named “type name + 0” as in chapter 1). Some ERROR lines are printed at startup, but it works anyway.
Terminal window
juiz --component ./target/debug/libexample_component.so -1 -d
  • A Component bundles several Processes, Containers and ContainerProcesses into one library (one file in Python).
  • In a configuration file, writing one Component name in components under plugins loads every type inside it.
  • Creating instances, IDs, calling and connecting all work the same as when types are loaded from separate files. A Component does not change how things run; it is the unit of distribution.

In the next chapter you connect juiz instances started separately, using only commands and no configuration file. Move on to Connecting separate processes.