Learn how to use class wrappers for cleaner, object-oriented plugin APIs in C#.
Classes in Plugify provide an object-oriented way to work with plugin resources, offering automatic lifecycle management and cleaner APIs. Instead of manually managing handles and calling functions with explicit pointers, you can use intuitive class-based interfaces with the IDisposable pattern.
Working with handle-based APIs in the traditional C-style can be verbose and error-prone:
Without Classes (C-style)
using example_plugin;
void ProcessConfig()
{
// Create a KeyValues handle manually
var kvHandle = example_plugin.example_plugin.Kv1Create("MyConfig");
// Set properties using the handle
example_plugin.example_plugin.Kv1SetName(kvHandle, "ServerSettings");
// Find a subkey
var subkeyHandle = example_plugin.example_plugin.Kv1FindKey(kvHandle, "Players");
// Easy to forget cleanup!
example_plugin.example_plugin.Kv1Destroy(kvHandle);
}
With classes, the same code becomes much cleaner and safer:
With Classes (OOP-style)
using example_plugin;
void ProcessConfig()
{
// Create using a constructor
using var kv = new KeyValues("MyConfig"); // Automatic cleanup when scope ends
// Use intuitive methods
kv.SetName("ServerSettings");
// Find a subkey - returns a KeyValues instance
using var subkey = kv.FindKey("Players");
// Automatic cleanup via using statement!
}
Benefits:
Cleaner syntax - Methods instead of functions with explicit handles
Automatic resource management - IDisposable pattern with deterministic cleanup
Using statement - Guarantees cleanup even if exceptions occur
Type safety - Better IDE autocomplete and compile-time checks
Thread-safe - SafeHandle uses DangerousAddRef/DangerousRelease for thread safety
Less error-prone - Harder to forget cleanup or use after disposal
Critical finalization - Finalizers run even during AppDomain unload
Exception safety - ObjectDisposedException when used after disposal
Important Pattern:
Use using for Cleanup: C# provides the using statement (or declaration) for deterministic resource cleanup. Always use using with IDisposable objects to ensure proper cleanup, even if exceptions occur.
Returns true if the handle is valid (not equal to invalidValue):
Using IsValid
using example_plugin;
void CheckHandle()
{
using var kv = new KeyValues("Config");
if (kv.IsValid)
{
kv.SetName("ServerConfig");
Console.WriteLine("Handle is valid");
}
else
{
Console.WriteLine("Handle is invalid");
}
// After release, handle becomes invalid
var handle = kv.Release();
Console.WriteLine(kv.IsValid); // false
}
Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:
Using Get()
using example_plugin;
void GetRawHandle()
{
using var kv = new KeyValues("Config");
// Get the raw handle
nint rawHandle = kv.Get();
// or
nint handle = kv.Handle;
Console.WriteLine($"Handle value: {rawHandle}");
// Pass to a function expecting a raw handle
example_plugin.example_plugin.Kv1GetName(kv.Get());
}
Warning: Be careful when using Get() or Handle. The returned handle is still owned by the class instance and will be destroyed when the instance is disposed.
Releases ownership of the handle and returns it. After calling Release(), the class instance becomes invalid and will not call the destructor:
Using Release()
using example_plugin;
nint CreateAndRelease()
{
var kv = new KeyValues("Config");
kv.SetName("ServerConfig");
// Transfer ownership out
nint handle = kv.Release();
// kv is now invalid, won't clean up
Console.WriteLine(kv.IsValid); // false
// No using needed - we released ownership
return handle;
}
void UseReleased()
{
// We now own the handle and must clean it up manually
nint rawHandle = CreateAndRelease();
// ... use rawHandle ...
example_plugin.example_plugin.Kv1Destroy(rawHandle); // Manual cleanup required!
}
The using statement provides deterministic cleanup and is the recommended pattern:
Using Statement (Recommended)
using example_plugin;
void ProcessConfig()
{
using var kv = new KeyValues("Config"); // C# 8.0+ using declaration
kv.SetName("ServerConfig");
kv.SetString("hostname", "My Server");
// ... use kv ...
// Automatically disposed when leaving scope
}
// Or traditional using block
void ProcessConfigTraditional()
{
using (var kv = new KeyValues("Config"))
{
kv.SetName("ServerConfig");
// ... use kv ...
} // Disposed here
}
This is the recommended pattern because:
Guaranteed cleanup even if exceptions occur
Clear scope of resource lifetime
Deterministic cleanup timing
Idiomatic C# code
Works with SafeHandle's critical finalizer
Best Practice: Always use using with IDisposable objects. In C# 8.0+, use using var declarations for cleaner code. The object will be disposed when leaving the scope.
SafeHandle provides critical finalization that runs even during AppDomain unload:
Critical Finalization
using example_plugin;
void ProcessConfig()
{
var kv = new KeyValues("Config");
kv.SetName("ServerConfig");
// ... use kv ...
// NO using - relies on finalization (NOT RECOMMENDED!)
}
// kv will eventually be finalized and cleaned up
// but timing is unpredictable - USE 'using' INSTEAD!
Important - Plugin Lifecycle: SafeHandle's critical finalizer runs even during AppDomain unload, providing a safety net. However, you must still use proper disposal patterns. If you store objects in fields, you are responsible for disposing them in OnPluginEnd(), otherwise behavior may be suboptimal.
Note: Critical finalizers in SafeHandle are more reliable than regular finalizers, but finalization timing is still non-deterministic. Always use using for predictable cleanup.
If a class doesn't have a destructor defined in the manifest, it's generated as a simple wrapper class without SafeHandle:
Wrapper Without Destructor
using example_plugin;
void UseWrapper()
{
// Class with no destructor - simple wrapper, no IDisposable
var wrapper = new SomeWrapper();
// Still has utility methods
if (wrapper.IsValid)
{
nint handle = wrapper.Get();
// ... use handle ...
}
// No automatic cleanup - handle persists
// Useful for stateless wrappers or global resources
}
When a method takes ownership of a resource, it calls Release() automatically:
Ownership Transfer
using example_plugin;
void TransferOwnership()
{
using var parent = new KeyValues("Parent");
var child = new KeyValues("Child");
// DON'T use 'using' with child - parent will take ownership
// AddSubKey takes ownership of child
parent.AddSubKey(child);
// child is now owned by parent
// child.IsValid is now false (Release() was called internally)
// parent will handle cleanup
}
Best practice: After transferring ownership, the object is automatically released:
Proper Ownership Handling
using example_plugin;
void ProperOwnership()
{
using var parent = new KeyValues("Parent");
var child = new KeyValues("Child");
// Transfer ownership - child.Release() called internally
parent.AddSubKey(child);
// child.IsValid is now false
// Only use through parent
using var found = parent.FindKey("Child");
}
When a method returns a new resource with ownership:
Return Ownership
using example_plugin;
void ReturnOwnership()
{
using var parent = new KeyValues("Parent");
// MakeCopy returns a NEW KeyValues that we own
using var copy = parent.MakeCopy();
if (copy.IsValid)
{
copy.SetName("ParentCopy");
// We're responsible for copy's lifecycle
// Disposed automatically by using
}
}
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
using example_plugin;
void NonOwningRef()
{
using var parent = new KeyValues("Parent");
// GetFirstSubKey returns a reference, parent still owns it
var childRef = parent.GetFirstSubKey();
if (childRef.IsValid)
{
// Use the reference, but DON'T dispose or release it
string name = childRef.GetName();
// childRef will be cleaned up by parent
// DON'T use 'using' with childRef
}
}
Important: With non-owning references, don't use using statement. The returned object is still valid (has a handle), but you don't own it. Disposing the parent will invalidate these references.
If you store class instances in fields, you must dispose them properly:
plugin.cs
using Plugify;
using example_plugin;
public class MyPlugin : Plugin
{
// Field - must be disposed!
private KeyValues? _config;
public void OnPluginStart()
{
// Create field instance
_config = new KeyValues("GlobalConfig");
_config.SetName("ServerSettings");
Console.WriteLine("Plugin started with global config");
}
public void OnPluginEnd()
{
// CRITICAL: Dispose fields before plugin unload
_config?.Dispose();
_config = null;
Console.WriteLine("Plugin ended, global config disposed");
}
}
Critical: Always dispose fields in OnPluginEnd(). While SafeHandle's critical finalizer provides a safety net, explicit disposal is still important for predictable resource cleanup.
public void OnCommand(string[] args)
{
// Local object with using - automatically disposed
using var kv = new KeyValues("TempConfig");
kv.SetName("CommandConfig");
// ... use kv ...
// Automatically disposed when leaving scope - safe!
}
✅ Safe: Method-local variables
Safe Pattern
public void ProcessData()
{
using var kv = new KeyValues("TempConfig");
kv.SetName("CommandConfig");
// ... use kv ...
// Disposed by using - safe!
}
✅ Safe: Field with disposal
Safe Pattern
public class MyPlugin : Plugin
{
private KeyValues? _pluginConfig;
public void OnPluginStart()
{
_pluginConfig = new KeyValues("PluginConfig");
_pluginConfig.SetName("Settings");
}
public void OnPluginEnd()
{
// Dispose fields
_pluginConfig?.Dispose();
_pluginConfig = null;
}
}
❌ Unsafe: Field without disposal
Unsafe Pattern
public class MyPlugin : Plugin
{
// DANGEROUS: Field without disposal
private KeyValues _config = new KeyValues("GlobalConfig");
public void OnPluginStart()
{
_config.SetName("ServerSettings");
}
public void OnPluginEnd()
{
// MISSING: No disposal!
// Relies on finalizer - not ideal!
}
}
❌ Unsafe: Static field without disposal
Unsafe Pattern
public class MyPlugin : Plugin
{
// DANGEROUS: Static field
private static Dictionary<string, KeyValues> _cache = new();
public void CacheConfig(string name)
{
_cache[name] = new KeyValues(name);
}
public void OnPluginEnd()
{
// MISSING: No cache cleanup!
// Should dispose all cached objects!
}
}
If constructor returns invalid handle, check the underlying C function:
Handle Constructor Failure
using example_plugin;
void HandleFailure()
{
var kv = new KeyValues("Config");
if (!kv.IsValid)
{
Console.WriteLine("Failed to create KeyValues");
kv.Dispose();
return;
}
using (kv)
{
// ... use kv ...
}
}
Be careful with non-owning references when the owner is disposed:
Dangling Reference Problem
using example_plugin;
KeyValues GetChildRef()
{
using var parent = new KeyValues("Parent");
var child = parent.GetFirstSubKey(); // Non-owning reference
return child; // BAD: parent will be disposed!
}
// This is dangerous!
void UseDanglingRef()
{
var childRef = GetChildRef();
// childRef now points to disposed memory
}
// Better approach:
KeyValues? GetChildOwned()
{
using var parent = new KeyValues("Parent");
using var child = parent.FindKey("Child"); // Non-owning reference
if (!child.IsValid)
{
return null;
}
// The owned copy outlives parent; the caller disposes it
return child.MakeCopy();
}