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 = example_plugin::Kv1Create("Config");
example_plugin::Kv1SetName(kv, "ServerConfig");
auto name = example_plugin::Kv1GetName(kv);
example_plugin::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
example_plugin::KeyValues kv("Config");
kv.SetName("ServerConfig");
auto name = kv.GetName();
// Automatically destroyed when kv goes out of scope!
Class (classes[]): Each entry describes one wrapper: its name, the handleType it wraps (ptr64 here) and the invalidValue that marks an empty handle ("0")
Constructors (constructors): Names of methods that return a new handle; each one becomes a class constructor
Destructor (destructor): Name of the method that releases the handle; it is called automatically when the object goes out of scope
Bindings (bindings): Methods exposed on the class. With "bindSelf": true the handle is passed as the first argument automatically and that parameter disappears from the generated signature
Ownership (owner): Set on a binding's paramAliases / retAlias entries; controls whether the class takes ownership of a parameter or return value
Transfers ownership to the caller and clears the internal handle. Use this when a function takes ownership of the object:
example_plugin::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
example_plugin::KeyValues kv1("Config1");
example_plugin::KeyValues kv2("Config2");
kv1.swap(kv2); // Now kv1 has Config2, kv2 has Config1
// Or using std::swap:
std::swap(kv1, kv2);
example_plugin::KeyValues kv1("Config1");
example_plugin::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() {
example_plugin::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() {
example_plugin::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:
example_plugin::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:
example_plugin::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:
example_plugin::KeyValues* dangling;
{
example_plugin::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():
example_plugin::KeyValues parent("Parent");
example_plugin::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 OnPluginEnd() to prevent crashes during plugin unload:
// DANGEROUS: Global object
example_plugin::KeyValues g_config;
plg::PluginResult OnPluginStart() override {
g_config = example_plugin::KeyValues("GlobalConfig");
// ... use config
return {};
}
plg::PluginResult OnPluginEnd() override {
// CRITICAL: Destroy global objects BEFORE plugin unloads!
g_config.reset(); // Or g_config = example_plugin::KeyValues{};
return {};
}
Why is this necessary?
C++ destructors for global objects run when your library is unloaded or the process exits, afterOnPluginEnd():
OnPluginEnd() is called
Plugify unloads the plugins; the plugin that exports Kv1Destroy() may already be gone
Global destructors of your library run
A global destructor calls Kv1Destroy() through a pointer to code that is no longer loaded
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:
example_plugin::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 = example_plugin::KeyValues("NewChild");
child.SetName("NewName"); // OK now