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.
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:
Resource Leaks: If you forget to call Kv1Destroy(), the resource leaks
Use-After-Free: You might accidentally use the handle after destroying it
No Type Safety: Raw handles (usize) provide no compile-time type checking
Manual Cleanup: You must manually track and destroy resources
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!
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
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
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
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
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
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();
});
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();
}
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) };
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
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.