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 test_keyvalues;
void ProcessConfig()
{
// Create a KeyValues handle manually
var kvHandle = test_keyvalues.Kv1Create("MyConfig");
// Set properties using the handle
test_keyvalues.Kv1SetName(kvHandle, "ServerSettings");
// Find a subkey
var subkeyHandle = test_keyvalues.Kv1FindKey(kvHandle, "Players");
// Easy to forget cleanup!
test_keyvalues.Kv1Destroy(subkeyHandle);
test_keyvalues.Kv1Destroy(kvHandle);
}
With classes, the same code becomes much cleaner and safer:
With Classes (OOP-style)
using test_keyvalues;
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 test_keyvalues;
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 test_keyvalues;
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
SomeCFunction(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 test_keyvalues;
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 ...
test_keyvalues.Kv1Destroy(rawHandle); // Manual cleanup required!
}
The using statement provides deterministic cleanup and is the recommended pattern:
Using Statement (Recommended)
using test_keyvalues;
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 test_keyvalues;
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 PluginEnd(), 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 test_keyvalues;
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 test_keyvalues;
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 test_keyvalues;
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 test_keyvalues;
void ReturnOwnership()
{
using var parent = new KeyValues("Parent");
// FindKey returns a NEW KeyValues that we own
using var child = parent.FindKey("Settings");
if (child.IsValid)
{
child.SetName("UpdatedSettings");
// We're responsible for child's lifecycle
// Disposed automatically by using
}
}
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
using test_keyvalues;
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 test_keyvalues;
public class MyPlugin : Plugin
{
// Field - must be disposed!
private KeyValues? _config;
public override void PluginStart()
{
// Create field instance
_config = new KeyValues("GlobalConfig");
_config.SetName("ServerSettings");
Log("Plugin started with global config");
}
public override void PluginEnd()
{
// CRITICAL: Dispose fields before plugin unload
_config?.Dispose();
_config = null;
Log("Plugin ended, global config disposed");
}
}
Critical: Always dispose fields in PluginEnd(). 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 override void PluginStart()
{
_pluginConfig = new KeyValues("PluginConfig");
_pluginConfig.SetName("Settings");
}
public override void PluginEnd()
{
// 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 override void PluginStart()
{
_config.SetName("ServerSettings");
}
public override void PluginEnd()
{
// 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 override void PluginEnd()
{
// MISSING: No cache cleanup!
// Should dispose all cached objects!
}
}
If constructor returns invalid handle, check the underlying C function:
Handle Constructor Failure
using test_keyvalues;
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 test_keyvalues;
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()
{
var parent = new KeyValues("Parent");
var child = parent.FindKey("Child"); // Returns owned instance
if (child == null || !child.IsValid)
{
parent.Dispose();
return null;
}
// Caller must dispose both parent and child
// Or keep parent alive
return child;
}