Learn how to use class wrappers for cleaner, object-oriented plugin APIs in C++.
Using Classes in C++
Classes in Plugify provide a more convenient, type-safe, and RAII-compliant way to work with complex objects exported by plugins. Instead of manually managing raw pointers and calling constructor/destructor functions, you can use generated C++ class wrappers that handle resource management automatically.
When a plugin exports functions that create and destroy objects (like Kv1Create and Kv1Destroy), you could call these functions directly:
// Manual approach - error-prone
void* kv = test_keyvalues::Kv1Create("Config");
test_keyvalues::Kv1SetName(kv, "ServerConfig");
auto 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
Exception Unsafety: If an exception is thrown, destructor won't be called
No Type Safety: Raw void* pointers provide no compile-time type checking
Verbose: You must manually pass the handle to every function call
Error-Prone: Easy to use the handle after it's been destroyed
Classes solve all these problems by using C++ RAII (Resource Acquisition Is Initialization):
// RAII approach - automatic, safe, exception-safe
test_keyvalues::KeyValues kv("Config");
kv.SetName("ServerConfig");
auto name = kv.GetName();
// Automatically destroyed when kv goes out of scope!
Transfers ownership to the caller and clears the internal handle. Use this when a function takes ownership of the object:
test_keyvalues::KeyValues subKey("SubSection");
parent.AddSubKey(std::move(subKey)); // AddSubKey calls release() internally
// After this call:
// - parent now owns the subKey resource
// - subKey object is moved and its handle is released
// - When parent is destroyed, it will also destroy the subKey
test_keyvalues::KeyValues kv1("Config1");
test_keyvalues::KeyValues kv2("Config2");
kv1.swap(kv2); // Now kv1 has Config2, kv2 has Config1
// Or using std::swap:
std::swap(kv1, kv2);
test_keyvalues::KeyValues kv1("Config1");
test_keyvalues::KeyValues kv2("Config2");
if (kv1 == kv2) { // Compare by handle
// Same underlying object
}
if (kv1 != kv2) {
// Different objects
}
// Spaceship operator for ordering
if (kv1 < kv2) {
// kv1's handle is less than kv2's
}
C++ classes use RAII (Resource Acquisition Is Initialization) for automatic resource management. This is the most deterministic approach among all languages supported by Plugify.
Resources are automatically destroyed when objects go out of scope:
void processConfig() {
test_keyvalues::KeyValues kv("ServerConfig");
kv.SetName("Production");
// Resource is automatically destroyed when kv goes out of scope
} // Destructor called here - Kv1Destroy() is invoked automatically
RAII guarantees cleanup even when exceptions are thrown:
void riskyOperation() {
test_keyvalues::KeyValues kv("Config");
performDatabaseOperation(); // Might throw
performNetworkOperation(); // Might throw
performFileOperation(); // Might throw
// If ANY of the above throws, kv's destructor is still called
// Resource is guaranteed to be cleaned up!
} // Destructor called even if exception was thrown
This is superior to other languages:
JavaScript: No deterministic cleanup, must use try-finally
Go: Must remember to use defer kv.Close()
C#: Must use using statement for deterministic cleanup
Python: Must use with statement or manually call __exit__()
When you create an object using the constructor, the class owns the resource and will destroy it:
test_keyvalues::KeyValues kv("Config");
// kv owns the resource (_ownership == Ownership::Owned)
// Destructor 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 the manifest), the class creates a borrowed instance:
test_keyvalues::KeyValues parent("Parent");
auto child = parent.FindKey("ChildKey");
// child is borrowed (_ownership == Ownership::Borrowed)
// child's destructor will NOT call Kv1Destroy()
// parent owns the actual child resource
Important: Borrowed objects must not outlive the object they were borrowed from:
test_keyvalues::KeyValues* dangling;
{
test_keyvalues::KeyValues parent("Parent");
auto child = parent.FindKey("Child");
dangling = &child; // DANGEROUS!
} // parent is destroyed here, taking child with it
// dangling now points to a destroyed object - undefined behavior!
Some methods take ownership of objects passed to them (marked with "owner": true in the manifest). These methods accept rvalue references and call release():
test_keyvalues::KeyValues parent("Parent");
test_keyvalues::KeyValues child("Child");
parent.AddSubKey(std::move(child));
// AddSubKey calls child.release() internally
// parent now owns child's resource
// child is now empty (handle == nullptr)
// WRONG: Don't use child after moving it
child.SetName("NewName"); // Throws: Empty handle!
// WRONG: Don't move from an lvalue without std::move
parent.AddSubKey(child); // Won't compile - needs rvalue reference
If you store class instances in global variables, static variables, or class fields, you must manually destroy them in pluginEnd() to prevent crashes during plugin unload:
// DANGEROUS: Global object
test_keyvalues::KeyValues g_config;
PLUGIN_EXPORT void pluginStart() {
g_config = test_keyvalues::KeyValues("GlobalConfig");
// ... use config
}
PLUGIN_EXPORT void pluginEnd() {
// CRITICAL: Destroy global objects BEFORE plugin unloads!
g_config.reset(); // Or g_config = test_keyvalues::KeyValues{};
}
Why is this necessary?
C++ destructors for global objects run after the plugin has unloaded:
pluginEnd() is called
Your plugin's shared library is unloaded from memory
Global destructors are called
Global destructor tries to call Kv1Destroy() - but the function is gone!
Problem: Methods throw "Empty handle" after moving an object.
Cause: After moving, the source object's handle is set to nullptr.
Solution: Don't use objects after moving them:
test_keyvalues::KeyValues child("Child");
parent.AddSubKey(std::move(child));
// child is now empty!
// Wrong: Using moved-from object
child.SetName("NewName"); // Throws: Empty handle
// Correct: Create new object if needed
child = test_keyvalues::KeyValues("NewChild");
child.SetName("NewName"); // OK now