Using Classes

Learn how to use class wrappers for cleaner, object-oriented plugin APIs in Rust.

Using Classes in Rust

Classes in Plugify provide a safe, idiomatic Rust way to work with complex objects exported by plugins. Instead of manually managing raw pointers and calling constructor/destructor functions, you can use generated Rust struct wrappers that handle resource management automatically using Rust's ownership system.

Why Use Classes?

When a plugin exports functions that create and destroy objects (like Kv1Create and Kv1Destroy), you could call these functions directly:

// Manual approach - error-prone and unsafe
let kv = test_keyvalues::Kv1Create(&Str::from("Config"));
test_keyvalues::Kv1SetName(kv, &Str::from("ServerConfig"));
let name = test_keyvalues::Kv1GetName(kv);
test_keyvalues::Kv1Destroy(kv);  // Easy to forget!

However, this approach has several problems:

  1. Resource Leaks: If you forget to call Kv1Destroy(), the resource leaks
  2. Use-After-Free: You might accidentally use the handle after destroying it
  3. No Type Safety: Raw handles (usize) provide no compile-time type checking
  4. Manual Cleanup: You must manually track and destroy resources
  5. Not Idiomatic Rust: Doesn't leverage Rust's ownership system

Classes solve all these problems by using Rust's ownership and Drop trait:

// RAII approach - automatic, safe
let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
kv.SetName(&Str::from("ServerConfig")).unwrap();
let name = kv.GetName().unwrap();
// Automatically destroyed when kv goes out of scope!

How Classes Work

The generator analyzes your plugin manifest and creates Rust struct wrappers for objects that have both constructor and destructor functions.

Generated Class Example

From a plugin manifest with Kv1Create and Kv1Destroy methods, the generator creates:

#[derive(Debug)]
pub enum KeyValuesError {
    EmptyHandle,
}

impl std::fmt::Display for KeyValuesError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            KeyValuesError::EmptyHandle => write!(f, "empty handle"),
        }
    }
}

impl std::error::Error for KeyValuesError {}

/// RAII wrapper for KeyValues handle.
#[derive(Debug)]
pub struct KeyValues {
    handle: usize,
    ownership: Ownership,
}

impl KeyValues {
    /// Creates a new KeyValues instance
    /// @param setName: The name to assign to this KeyValues instance
    #[allow(dead_code, non_snake_case)]
    pub fn new(setName: &Str) -> Result<Self, KeyValuesError> {
        let h = crate::test_keyvalues::Kv1Create(setName);
        if h == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(Self {
            handle: h,
            ownership: Ownership::Owned,
        })
    }

    /// Construct from raw handle with specified ownership
    #[allow(dead_code)]
    pub unsafe fn from_raw(handle: usize, ownership: Ownership) -> Self {
        Self { handle, ownership }
    }

    /// Returns the underlying handle
    #[allow(dead_code)]
    pub fn get(&self) -> usize {
        self.handle
    }

    /// Release ownership and return the handle. Wrapper becomes empty & borrowed.
    #[allow(dead_code)]
    pub fn release(&mut self) -> usize {
        let h = self.handle;
        self.handle = 0;
        self.ownership = Ownership::Borrowed;
        h
    }

    /// Destroys and resets the handle
    #[allow(dead_code)]
    pub fn reset(&mut self) {
        if self.handle != 0 && self.ownership == Ownership::Owned {
            crate::test_keyvalues::Kv1Destroy(self.handle);
        }
        self.handle = 0;
        self.ownership = Ownership::Borrowed;
    }

    /// Swaps two KeyValues instances
    #[allow(dead_code)]
    pub fn swap(&mut self, other: &mut KeyValues) {
        std::mem::swap(&mut self.handle, &mut other.handle);
        std::mem::swap(&mut self.ownership, &mut other.ownership);
    }

    /// Returns true if handle is valid (not empty)
    #[allow(dead_code)]
    pub fn is_valid(&self) -> bool {
        self.handle != 0
    }

    /// Gets the section name of a KeyValues instance
    /// @return The name of the KeyValues section
    #[allow(dead_code, non_snake_case)]
    pub fn GetName(&self) -> Result<Str, KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(crate::test_keyvalues::Kv1GetName(self.handle))
    }

    /// Sets the section name of a KeyValues instance
    /// @param name: The new name to assign to this KeyValues section
    #[allow(dead_code, non_snake_case)]
    pub fn SetName(&self, name: &Str) -> Result<(), KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        crate::test_keyvalues::Kv1SetName(self.handle, name);
        Ok(())
    }

    /// Finds a key by name
    /// @param keyName: The name of the key to find
    /// @return Pointer to the found KeyValues subkey, or NULL if not found
    #[allow(dead_code, non_snake_case)]
    pub fn FindKey(&self, keyName: &Str) -> Result<KeyValues, KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(unsafe { KeyValues::from_raw(crate::test_keyvalues::Kv1FindKey(self.handle, keyName), Ownership::Borrowed) })
    }

    /// Adds a subkey to this KeyValues instance
    /// @param subKey: Pointer to the KeyValues object to add as a child
    #[allow(dead_code, non_snake_case)]
    pub fn AddSubKey(&mut self, subKey: KeyValues) -> Result<(), KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        crate::test_keyvalues::Kv1AddSubKey(self.handle, subKey.release());
        Ok(())
    }

}

impl Drop for KeyValues {
    fn drop(&mut self) {
        if self.handle != 0 && self.ownership == Ownership::Owned {
            crate::test_keyvalues::Kv1Destroy(self.handle);
        }
    }
}

impl std::cmp::PartialEq for KeyValues {
    fn eq(&self, other: &Self) -> bool {
        self.handle == other.handle
    }
}
impl std::cmp::Eq for KeyValues {}

impl std::cmp::PartialOrd for KeyValues {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        (self.handle).partial_cmp(&(other.handle))
    }
}
impl std::cmp::Ord for KeyValues {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        (self.handle).cmp(&(other.handle))
    }
}

Resource Management with RAII

Rust classes use RAII (Resource Acquisition Is Initialization) for automatic resource management. This provides compile-time guaranteed safety.

Automatic Cleanup

Resources are automatically destroyed when objects go out of scope:

fn process_config() {
    let kv = test_keyvalues::KeyValues::new(&Str::from("ServerConfig")).unwrap();
    kv.SetName(&Str::from("Production")).unwrap();
    // Resource is automatically destroyed when kv goes out of scope
} // Drop trait called here - Kv1Destroy() is invoked automatically

Ownership and Borrowing

Rust's ownership system prevents common errors:

let kv1 = test_keyvalues::KeyValues::new(&Str::from("Config1")).unwrap();
let kv2 = kv1;  // kv1 moved to kv2
// let name = kv1.GetName();  // ERROR: kv1 was moved

// Correct: borrow instead
let kv1 = test_keyvalues::KeyValues::new(&Str::from("Config1")).unwrap();
let name = kv1.GetName().unwrap();  // Borrow
// kv1 is still valid here

Borrowing vs Moving

// Borrowing (read-only access)
fn read_config(kv: &KeyValues) {
    let name = kv.GetName().unwrap();
}

// Mutable borrowing (modify)
fn modify_config(kv: &KeyValues) {
    kv.SetName(&Str::from("Modified")).unwrap();
}

// Taking ownership (consumes the value)
fn consume_config(kv: KeyValues) {
    // kv is consumed here
}

let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
read_config(&kv);       // Borrow
modify_config(&kv);     // Mutable borrow
consume_config(kv);     // Move
// kv is no longer valid here

Ownership Semantics

Owned Resources

When you create an object using new(), the struct owns the resource:

let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
// kv owns the resource (ownership = Ownership::Owned)
// Drop will call Kv1Destroy() when kv goes out of scope

Borrowed Resources

When a method returns a pointer that you don't own (marked with "owner": false in manifest):

let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
let child = parent.FindKey(&Str::from("ChildKey")).unwrap();
// child is borrowed (ownership = Ownership::Borrowed)
// child's Drop will NOT call Kv1Destroy()
// parent owns the actual child resource

Important: Borrowed objects must not outlive the object they were borrowed from:

// WRONG - Dangling reference
let child = {
    let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
    parent.FindKey(&Str::from("Child")).unwrap()
}; // parent is destroyed here, taking child with it
// child now points to destroyed memory!

// Rust's borrowing rules help prevent this at compile time

Ownership Transfer

Some methods take ownership of objects (marked with "owner": true in manifest):

let mut parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
let child = test_keyvalues::KeyValues::new(&Str::from("Child")).unwrap();

parent.AddSubKey(child).unwrap();
// child is moved into AddSubKey
// AddSubKey internally calls release() to transfer ownership to parent
// child variable is no longer valid

// WRONG: Can't use child after moving
// child.SetName(&Str::from("NewName"));  // ERROR: value used after move

Working with Option

For optional class instances:

fn find_config(name: &str) -> Option<KeyValues> {
    let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).ok()?;
    let child = parent.FindKey(&Str::from(name)).ok()?;

    if child.is_valid() {
        Some(child)
    } else {
        None
    }
}

match find_config("MyConfig") {
    Some(kv) => println!("Found: {}", kv.GetName().unwrap()),
    None => println!("Not found"),
}

Thread Safety

Rust's type system enforces thread safety:

use std::sync::{Arc, Mutex};

// Not thread-safe by default
let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();

// Make it thread-safe
let kv = Arc::new(Mutex::new(
    test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap()
));

// Clone Arc for threads
let kv_clone = Arc::clone(&kv);
std::thread::spawn(move || {
    let kv = kv_clone.lock().unwrap();
    kv.SetName(&Str::from("ThreadModified")).unwrap();
});

Complete Example

Here's a complete example demonstrating all concepts:

use plugify::*;

fn on_plugin_start() {
    // Create owned object
    let mut root_config = test_keyvalues::KeyValues::new(&Str::from("ServerConfig")).unwrap();
    root_config.SetName(&Str::from("Production")).unwrap();

    // Create subkeys and transfer ownership
    let database = test_keyvalues::KeyValues::new(&Str::from("Database")).unwrap();
    database.SetName(&Str::from("PostgreSQL")).unwrap();
    root_config.AddSubKey(database).unwrap();
    // database is no longer valid - ownership transferred

    let caching = test_keyvalues::KeyValues::new(&Str::from("Caching")).unwrap();
    caching.SetName(&Str::from("Redis")).unwrap();
    root_config.AddSubKey(caching).unwrap();

    // Find returns borrowed reference
    let db_config = root_config.FindKey(&Str::from("Database")).unwrap();
    if db_config.is_valid() {
        println!("Database: {}", db_config.GetName().unwrap());
        // db_config is borrowed - root_config still owns it
    }

    // root_config automatically destroyed when function ends
}

register_plugin!(
    start: on_plugin_start
);

Best Practices

  1. Let Rust manage lifetimes: Trust the borrow checker
    fn process() {
        let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
        // Use kv...
    } // Automatically destroyed
    
  2. Use references for read-only access: Avoid unnecessary moves
    fn read_config(kv: &KeyValues) {
        let name = kv.GetName().unwrap();
    }
    
  3. Use references for modifications: Methods return Result
    fn modify_config(kv: &KeyValues) {
        kv.SetName(&Str::from("Modified")).unwrap();
    }
    
  4. Check for valid handles: Especially with borrowed objects
    let child = parent.FindKey(&Str::from("Child")).unwrap();
    if child.is_valid() {
        child.SetName(&Str::from("NewName")).unwrap();
    }
    
  5. Don't mix ownership models: Stick to Rust's ownership system
    // Good
    let kv = KeyValues::new(&Str::from("Config")).unwrap();
    
    // Avoid unless necessary
    let mut kv = KeyValues::new(&Str::from("Config")).unwrap();
    let raw = kv.release();
    unsafe { KeyValues::from_raw(raw, Ownership::Owned) };
    

Advantages of Rust's Ownership System

Rust's class wrappers provide the safest resource management among all Plugify languages:

  1. Compile-Time Safety: Most errors caught at compile time, not runtime
  2. Zero-Cost Abstractions: No runtime overhead for safety guarantees
  3. No Garbage Collection: Deterministic cleanup without GC pauses
  4. Memory Safety: No use-after-free, double-free, or null pointer dereferences
  5. Thread Safety: Data races prevented at compile time
  6. Explicit Ownership: Clear transfer of responsibility

This makes Rust the most reliable language for working with Plugify classes, catching errors before they can cause problems in production.

Troubleshooting

Empty Handle Error

Problem: KeyValuesError::EmptyHandle when calling methods.

Cause: Using a borrowed object with invalid handle (handle == 0).

Solution: Check for validity before use:

let child = parent.FindKey(&Str::from("Child")).unwrap();
if child.is_valid() {
    child.SetName(&Str::from("NewName")).unwrap();
}

Use After Move

Problem: Compiler error "value used after move".

Cause: Trying to use a value after transferring ownership.

Solution: Don't use values after moving them:

let child = KeyValues::new(&Str::from("Child")).unwrap();
parent.AddSubKey(child).unwrap();
// Don't use child here - it was moved

Lifetime Issues

Problem: Compiler error about lifetimes.

Cause: Borrowed reference outlives the owner.

Solution: Ensure borrowed values don't outlive their owners:

let db_config = {
    let parent = KeyValues::new(&Str::from("Parent")).unwrap();
    parent.FindKey(&Str::from("Database")).unwrap()  // ERROR: parent dropped here
};

// Correct version
let parent = KeyValues::new(&Str::from("Parent")).unwrap();
let db_config = parent.FindKey(&Str::from("Database")).unwrap();
// Use db_config while parent is still alive

Conclusion

Rust's class wrappers leverage the language's ownership system to provide the safest and most reliable way to work with Plugify classes. The borrow checker ensures memory safety at compile time, preventing entire classes of runtime errors. By following Rust's ownership principles and the best practices in this guide, you can build robust plugins with confidence.