Using Kanzi Rust API

Kanzi Rust API crate implements platform-independent proxies for Kanzi classes, providing rich access to the Kanzi functionality in Rust. This enables you to use Kanzi seamlessly on platforms where Rust is the native language.

This topic provides an overview of the fundamentals of Kanzi Rust API. For complete API reference, see Kanzi Rust API reference.

Requirements

Rust toolchain 1.83.0 or newer. See https://www.rust-lang.org/tools/install.

Creating instances

Kanzi Rust API structs come with create methods that you can use to create instances.

Managing instance lifetime

Kanzi Rust API uses Rust ownership model to manage the lifetime of native objects. Each Rust instance shares ownership with the native backend, acting as a strong reference. When a Rust instance is dropped and no native references exist, the object is reclaimed.

Additionally, Kanzi Rust API provides Weak reference to the native object that does not affect its lifetime. You can pass it to any function that accepts an Object. If an application accesses a weak reference after the native object has been reclaimed, the function returns a StaleObject error from ErrorKind.

// Create an `EmptyNode2D` node.
let node = kanzi::EmptyNode2D::create(domain, "My Empty Node")?;

// Use `EmptyNode2D` instance from the strong reference.
node.set_name("Name")?;

// Create a weak reference to the instance of `EmptyNode2D` node.
let node_weak = node.downgrade_ref();

// Use `EmptyNode2D` instance from the weak reference.
node_weak.set_name("Weak Name")?;

// Destroy the instance of the node.
std::mem::drop(node);

// Trying to access the node after all strong references were destroyed will result
// in a `kanzi::ErrorKind::StaleObject` error.
let result = node_weak.set_name("New Name");
let error = result.expect_err("The `node` was dropped");
assert!(error.is_stale_object());

// Even though the error was returned. This type of error is soft and the execution can
// continue.
assert!(error.is_soft_error());

You can upgrade a weak reference to a strong reference using the upgrade method of Weak. If the native object is still alive, the method returns a strong reference to it. If the object has been reclaimed, it returns an error.

// Access a weak reference to a `kanzi::Screen` child.
let node: kanzi::Weak<kanzi::Node2D> = screen.get_child(0)?;

// Upgrade the weak reference to start owning the object.
// For cases when the object was already destroyed `Option` is returned.
//
// Requires allocating.
if let Some(child) = node.clone().upgrade()? {
    child.set_name("Updated Name")?;
}

// Use the weak reference and handle `kanzi::ErrorKind::StaleObject` error.
//
// Doesn't require allocating.
match node.set_name("Updated Name") {
    Ok(()) => (),
    Err(error) if error.is_stale_object() => (),
    error => return error,
};

Using inheritance and object casting

Kanzi Rust API emulates C++ style inheritance and type casting for its structs, following a class hierarchy similar to Kanzi C++ API. Any Kanzi Rust API struct that supports inheritance implements Inherits trait for upcasting to its parent type and ObjectConstraint trait for downcasting to a specific type.

let node2d = screen.get_child(0)?;
// Downcast to a more concrete type.
let viewport2d = node2d
    .downcast::<kanzi::Viewport2D>()?
    .into_error(kanzi::ErrorKind::TypeMismatch)?;

// Call a function from a base type.
let is_enabled = viewport2d.get_enabled()?;
assert!(is_enabled);

// Upcast to a base type.
let node2d: kanzi::Weak<kanzi::Node2D> = viewport2d.upcast();
node2d.set_enabled(false)?;

Thread-safety

Kanzi Rust API is designed to be thread-safe in a multi-threaded context. It enforces compile time restrictions that prevent storing objects capable of modifying Kanzi state outside the Kanzi thread.

To store references to Kanzi objects on a non-Kanzi thread, use the ThreadObject abstraction. ThreadObject is a thread-safe weak reference to a Kanzi object. This reference type does not allow direct method calls. To call methods, unlock the ThreadObject by passing a Domain as an argument. This statically ensures that unlocking happens only on the Kanzi thread:

let handle = std::thread::spawn({
    // Define the state which will be sent to a different thread.

    // Construct `TaskDispatcher` out of `Domain`. `TaskDispatcher` can be sent to other threads.
    let task_dispatcher = domain.task_dispatcher();

    // `lock` takes ownership of the object. If there is only a single instance of the object it
    // will immediately become stale, in such cases `lock_ref` can be used instead.
    let screen = screen.lock();

    move || {
        task_dispatcher.submit(move |domain| {
            // `unlock` the object by passing `Domain` to it to make it usable.
            let screen = screen.unlock(domain);
            screen.set_name("New Screen Name")?;

            Ok(())
        });
    }
});

All instances of ThreadObject should be dropped before Kanzi Rust API is unloaded otherwise those object will be leaked, since the Rust runtime will not be available any longer.

Using properties

Kanzi Rust API modules contain static variables for built-in property types such as VISIBLE_PROPERTY. To read and write properties:

let property_type = &kanzi::coreui::node::node::VISIBLE_PROPERTY;

// Set property.
screen.set_property(property_type, true)?;

// Get property.
assert!(screen.get_property(property_type)?);

In the snippet above, the full property path is used: kanzi::coreui::node::node::VISIBLE_PROPERTY. This path matches Kanzi C++ header paths. However, full paths can be verbose, so a shortcut is provided. To derive the shortcut name, convert the class name to snake_case (for example, Node2D becomes node_2d).

let property_type = &kanzi::screen::VISIBLE_PROPERTY;
screen.set_property(property_type, true)?;

This can be further simplified by getters and setters, which are available for all built-in property types:

// Set `VISIBLE_PROPERTY` property.
screen.set_visible(true)?;

// Get `VISIBLE_PROPERTY` property.
assert!(screen.get_visible()?);

To access a property by name, create an instance of PropertyType or AbstractPropertyType with a full name of the property type:

// Use `kanzi::PropertyType::<T>` when the data type is known.
let property_type = kanzi::PropertyType::<bool>::find(domain, "Node.Visible")?;
screen.set_property(&property_type, true)?;

// Use `kanzi::AbstractPropertyType` when the data type is not known.
let property_type = kanzi::AbstractPropertyType::find(domain, "Node.Visible")?
    .into_critical("Property not found")?;
// Casting will fail if the data type doesn't match the one provided.
if let Some(property_type) = property_type.cast::<bool>()? {
    screen.set_property(&property_type, true)?;
}

To define a new property type, declare it with the property! procedural macro:

kanzi::property!(CustomProperty {
    data_type: kanzi::KanziString,
    name: "Custom",
    default_value: "default_value",
    flags: kanzi::ChangeFlags::FinalTransformation,
    inheritable: false,
});

// The macro generates a global accessor to the property.
screen.set_property(&CUSTOM_PROPERTY, "custom value".into())?;
// `CustomProperty` can now be found like any other property.
let property_type = kanzi::PropertyType::<kanzi::KanziString>::find(domain, "Custom")?;
assert_eq!(screen.get_property(&property_type)?, "custom value");

Properties are typically defined as part of a class, as shown in Creating a custom type. Note that when properties are defined as a part of a class, they will be registered automatically when the class itself is registered.

Using Kanzi Rust API enums

Kanzi Rust API comes with the set of enums associated with the built-in properties.

For setter and getter functions, access enum values like this:

screen.set_property(
    &kanzi::text_block_2d::FONT_HINTING_PREFERENCE_PROPERTY,
    kanzi::FontHintingPreference::NativeHinting,
)?;

screen.get_property(&kanzi::text_block_2d::FONT_HINTING_PREFERENCE_PROPERTY)?;

Using messages

Using built-in messages

Kanzi Rust API modules contain static variables for built-in message types, such as TOGGLE_STATE.

To dispatch a message:

// Construct message arguments
let message_type = &kanzi::toggle_button_3d::TOGGLE_STATE;
let args = message_type.create_args(domain)?;

// Set message arguments
args.set_toggle_state(1)?;
button.dispatch_message(message_type, &args)?;

To react when a node receives a message, add a message handler to the node:

// Add message subscription.
button.add_message_handler(&kanzi::button_3d::TOGGLE_STATE, |args| {
    // Extracting arguments through generic accessors.
    let toggle_state = args.get_toggle_state()?;
    assert_eq!(toggle_state, 1);

    Ok(())
})?

To use a message by name, create an instance of AbstractMessageType with the full name of the message type:

// Get reference to a message type.
let message_type =
    kanzi::AbstractMessageType::find(domain, "Message.ToggleButton.ToggleState")?
        .into_critical("MessageType not found")?;

// Construct default message arguments.
let args = kanzi::MessageArguments::create(domain)?;

// Set message arguments.
args.set_argument(&kanzi::button_3d::TOGGLE_STATE_PROPERTY, 1)?;

// Send the message
node.dispatch_message(&message_type, &args)?;

Using custom messages

To define a new message type, declare it with the message! procedural macro:

kanzi::message!(CustomMessage {
    arguments_type: kanzi::MessageArguments,
    name: "CustomMessage",
    routing: kanzi::MessageRouting::Tunneling,
});

Access it through the static variable generated by the macro, and use it to create and dispatch messages:

// Get reference to the message type.
let message_type = CUSTOM_MESSAGE_LOCAL
    .get()
    .into_critical("Message not initialized")?;

// Create message arguments.
let args = message_type.create_args(domain)?;

// Set message arguments.
args.set_argument(&kanzi::node::VISIBLE_PROPERTY, true)?;

// Send the message.
node.dispatch_message(message_type, &args)?;

Messages are typically defined as part of a class, as shown in Creating a custom type. Note that when sessages are defined as a part of a class, they will be registered automatically when the class itself is registered.

Creating a custom type

You can use Rust to extend the functionality of Kanzi Engine. Kanzi Rust API provides several procedural macros that you can use to derive from one of the extensible Kanzi types.

For example, to create a custom 2D node:

  1. Declare a new struct with the class procedural macro, use metaclass attributes to define a Kanzi metaclass information of your custom node.

    #[kanzi::class]
    #[metaclass(name = "Example.MyNode2D")]
    #[metaclass(base = kanzi::Node2D)]
    #[metaclass(property_type = MyProperty::<kanzi::KanziString>)]
    struct MyNode2D {
        // Define custom state.
        value: bool,
    }
    
  2. Add a constructor for your type into impl block annotated with the state macro.

    #[kanzi::state]
    impl MyNode2D {
        fn new(_domain: &kanzi::Domain, _name: &kanzi::KanziStr) -> kanzi::Result<Self> {
            // Add custom initialization code.
    
            // Return initialized object.
            Ok(Self { value: true })
        }
    }
    
  3. (Optional) Create custom property types. Note that the properties are tied to the class through metaclass(property_type) attributes.

    kanzi::property!(MyProperty {
        data_type: kanzi::KanziString,
        name: "MyNode2D.MyProperty",
        default_value: "default_value",
        flags: kanzi::ChangeFlags::empty(),
        inheritable: false,
    });
    
  4. (Optional) Create custom message arguments, types, and handlers.

  5. (Optional) Define custom node behavior by overriding relevant methods in the impl block annotated with the overrides.

    #[kanzi::overrides]
    impl kanzi::INode2D for MyNode2D {
        fn arrange_override(&self, actual_size: kanzi::Vector2) -> kanzi::Result<()> {
            // Define custom behavior.
    
            // Call base method.
            self.base().arrange_override(actual_size)?;
    
            Ok(())
        }
    }
    
  6. (Optional) Define custom methods in the impl block annotated with the methods macro.

    #[kanzi::methods]
    impl MyNode2D {
        fn get_value(&self) -> bool {
            self.state_ref().value
        }
    }
    

Make sure to register your custom type before using it. Once registered, you can work with it just like any other Kanzi Rust API type.

// The object needs to be registered before it can be created.
MyNode2D::register(domain)?;

// Create an instance of the `MyNode2D`.
let my_node = MyNode2D::create(domain, "MyNode2D")?;

// Call any method from `Node2D` inheritance chain.
my_node.arrange()?;

// Call custom methods.
assert!(my_node.get_value());

// Set custom property.
my_node.set_my_property("dynamic_value")?;

Implementing custom resource loading

To implement custom resource loading, implement a ResourceProtocol trait for your struct.

struct MyProtocol {
    domain: kanzi::Domain,
    should_load_synchronously: bool,
}

impl kanzi::ResourceProtocol for MyProtocol {
    fn handle(
        &self,
        _url: &kanzi::KanziStr,
        _protocol: &kanzi::KanziStr,
        _hostname: &kanzi::KanziStr,
        _path: &kanzi::KanziStr,
    ) -> kanzi::Result<Either<kanzi::Resource, Box<dyn kanzi::ResourceLoadTask>>> {
        let resource: Either<kanzi::Resource, Box<dyn kanzi::ResourceLoadTask>> =
            match self.should_load_synchronously {
                true => Either::L(self.load_synchronous_resource()?),
                false => Either::R(Box::new(MyLoadTask {})),
            };
        Ok(resource)
    }
}

struct MyLoadTask {}
impl kanzi::ResourceLoadTask for MyLoadTask {
    fn load<'a>(&'a mut self, _enqueue_dependencies: &'a dyn Fn(&[&kanzi::KanziStr])) {
        // Perform thread-independent loading.
    }

    fn finish(&self, domain: &kanzi::Domain) -> kanzi::Result<kanzi::Resource> {
        // Return the created resource. It will be stored automatically.
        self.create_resource(domain)
    }

    fn get_type(&self) -> kanzi::ResourceLoadTaskType {
        // Inform the runtime that thread-independent loading is required.
        kanzi::ResourceLoadTaskType::LoadAndFinish
    }
}

domain.resource_manager().register_protocol_handler(
    "MyProtocol",
    MyProtocol {
        domain: domain.clone(),
        should_load_synchronously: false,
    },
)?;

By default the ResourceLoadTask calls both the load and the finish. To set the ResourceLoadTask to call only finish, return FinishOnly from the get_type function.

Resource protocols can support the reload operation by implementing the ResourceReloadProtocol. Kanzi calls the reload handler to reinitialize an existing resource when the OpenGL context is invalidated.

struct MyReloadProtocol {
    domain: kanzi::Domain,
}

impl kanzi::ResourceProtocol for MyReloadProtocol {
    fn handle(
        &self,
        _url: &kanzi::KanziStr,
        _protocol: &kanzi::KanziStr,
        _hostname: &kanzi::KanziStr,
        _path: &kanzi::KanziStr,
    ) -> kanzi::Result<Either<kanzi::Resource, Box<dyn kanzi::ResourceLoadTask>>> {
        let texture = kanzi::Texture::create(
            &self.domain,
            kanzi::texture::TextureCreateInfo {
                width: 128,
                height: 128,
                ..empty_texture_create_info()
            },
            "My Texture",
        )?;
        Ok(Either::L(texture.upcast()))
    }
}

impl kanzi::ResourceReloadProtocol for MyReloadProtocol {
    fn handle(
        &self,
        _url: &kanzi::KanziStr,
        _protocol: &kanzi::KanziStr,
        _hostname: &kanzi::KanziStr,
        _path: &kanzi::KanziStr,
        resource: kanzi::Weak<kanzi::Resource>,
    ) -> kanzi::Result<()> {
        let Some(texture) = resource.downcast::<kanzi::Texture>()? else {
            return Ok(());
        };

        texture.recreate(kanzi::texture::TextureCreateInfo {
            width: 128,
            height: 128,
            ..empty_texture_create_info()
        })?;

        Ok(())
    }
}

Creating a data source

A data source enables you to access third-party data from your Kanzi application.

To create a data source:

  1. Declare a new struct with the class procedural macro, use metaclass attributes to define a Kanzi metaclass information of your data source.

    #[kanzi::class]
    #[metaclass(name = "MyPlugin.MyDataSource")]
    #[metaclass(base = kanzi::DataSource)]
    pub struct MyDataSource {
        root: kanzi::AbstractDataObject,
        data: kanzi::DataObject<bool>,
    }
    
  2. Add a constructor for your data source into impl block annotated with the state macro.

    #[kanzi::state]
    impl MyDataSource {
        fn new(domain: &kanzi::Domain, _name: &kanzi::KanziStr) -> kanzi::Result<Self> {
            let root = kanzi::AbstractDataObject::create(domain, "root")?;
    
            // Add the remaining structure of `MyDataSource`.
            let data = kanzi::DataObject::<bool>::create(domain, "MyBoolean", true)?;
            root.add_child(&data)?;
    
            Ok(Self { root, data })
        }
    }
    
  3. Override data source methods in the impl block annotated with the overrides macro. The state defined inside of the struct is not accessible directly and is wrapped in RefCell; state_mut and state_ref functions are generated to access it instead. This is because Rust, unlike C++, doesn’t allow aliasing, which can happen when the method calls can callback into another methods defined in Rust.

    #[kanzi::overrides]
    impl kanzi::IDataSource for MyDataSource {
        fn get_data(&self) -> kanzi::Result<Option<kanzi::Weak<kanzi::AbstractDataObject>>> {
            // Immutably borrow the state inside of the `MyDataSource` definition.
            let state = self.state_ref();
            // Get a weak reference to a data object stored inside of the state.
            let data: kanzi::Weak<kanzi::DataObject<bool>> = state.data.downgrade_ref();
            // Upcast to a type required by the function signature.
            let data: kanzi::Weak<kanzi::AbstractDataObject> = data.upcast();
    
            Ok(Some(data))
        }
    }
    

Make sure to register your custom type before using it.

Creating Kanzi Rust application

The main entry point to Kanzi remains a C++ Application class, because subsystems for configuration and rendering are platform-dependent and available only in C++.

You can integrate application logic written in Rust into your Kanzi application using the bridge procedural macro. This macro allows you to define a Rust struct whose constructors and methods are automatically exposed as a corresponding C++ class. The generated bridge class enables seamless invocation of Rust logic from C++, eliminating the need for manual FFI bindings.

See how to Create a Kanzi Rust application using a template and Build a Kanzi Rust application

To create a bridge class:

  1. Declare a new struct.

    pub struct Application {
        domain: kanzi::Domain,
        initialized: bool,
    }
    
  2. Define an impl block annoted with the bridge procedural macro. Provide the header_path attribute to specify where the C++ header will be generated.

    #[kanzi::bridge(header_path = "./tests/snippets/custom_application.hpp")]
    impl Application {
    
  3. Define a constructor with an initialization logic. The first argument should be &kanzi::Domain. You can also add arguments of any type supported by bridge macro.

    pub fn new(domain: &kanzi::Domain) -> kanzi::Result<Self> {
        // On initialization register all classes provided by the application.
        MyNode2D::register(domain)?;
        MyDataSource::register(domain)?;
    
        Ok(Self {
            domain: domain.clone(),
            initialized: false,
        })
    }
    
  4. (Optional) Define additional methods in the impl block, which should be exposed to C++.

    pub fn on_project_loaded(&mut self, screen: Weak<kanzi::Screen>) -> kanzi::Result<()> {
        let node = MyNode2D::create(&self.domain, "Screen.MyNode2D")?;
        screen.add_child(&node)?;
    
        self.initialized = true;
    
        Ok(())
    }
    
  5. Build the crate with the generate_bridge feature passed to Kanzi crate. The header file will be generated at the path specified by header_path attribute.

    namespace application
    {
    namespace rust
    {
    
    class Application
    {
    public:
        explicit Application(Domain* native_domain);
    
        ~Application();
    
        Application(Application&&) noexcept = delete;
        Application& operator=(Application&&) noexcept = delete;
        Application(const Application&) = delete;
        Application& operator=(const Application&) = delete;
    
        void onProjectLoaded(shared_ptr<Screen> screen);
    };
    
    }
    }
    
  6. Use the class in your C++ application code.

    class MyApplication : public Application
    {
    public:
        void onStartup() override
        {
            m_application = make_unique<rust::Application>(getDomain());
        }
    
        void onProjectLoaded() override
        {
            m_application->onProjectLoaded(getScreen());
    
        }
    
        void onShutdown() override
        {
            m_application.reset();
        }
    
        ...
    
    private:
        unique_ptr<rust::Application> m_application;
    };
    

For a complete example on creating a Rust application, see Hello Rust.

Creating Kanzi Engine Rust plugins

You can separate your Rust types into Kanzi Engine Rust plugins. This allows you to use these types from Kanzi Studio and reuse them between multiple applications.

See how to Create a Kanzi Engine Rust plugin using a template and Build a Kanzi Engine Rust plugin

To create a Kanzi Engine plugin in Rust:

  1. Declare a new struct with the plugin procedural macro, use plugin attribute to define a name of your Rust plugin.

    #[kanzi::plugin]
    #[plugin(name = "my_plugin")]
    #[derive(Default)]
    pub struct MyPlugin;
    
  2. Register your custom types with Kanzi.

    impl kanzi::IPlugin for MyPlugin {
        fn register_metadata_override(&self, domain: &kanzi::Domain) -> kanzi::Result<()> {
            MyDataSource::register(domain)?;
    
            Ok(())
        }
    }
    

It is recommended to use templates for creating plugins, since those will generate the proper Cargo.toml configuration and custom build steps to allow compiling plugin both into a dynamic and a static library.

See also

Kanzi Rust API reference

Using Kanzi Rust API in Kanzi application

Using Kanzi Engine Rust plugins