Learn how to use class wrappers for cleaner, object-oriented plugin APIs in Go.
Classes in Plugify provide an object-oriented way to work with plugin resources, offering lifecycle management and cleaner APIs. Instead of manually managing handles and calling functions with explicit pointers, you can use intuitive struct-based interfaces with idiomatic Go error handling.
Use Defer for Cleanup: Go doesn't have destructors like C++, but defer provides deterministic cleanup within function scope. Always use defer obj.Close() after creating owned objects.
Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:
Using Get()
package main
import (
"fmt"
"test_keyvalues"
)
func getRawHandle() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
defer kv.Close()
// Get the raw handle
rawHandle := kv.Get()
fmt.Printf("Handle value: %d\n", rawHandle)
// Pass to a function expecting a raw handle
someCFunction(kv.Get())
}
Warning: Be careful when using Get(). The returned handle is still owned by the struct instance and will be destroyed when the instance is cleaned up.
Releases ownership of the handle and returns it. After calling Release(), the struct instance becomes invalid and will not call the destructor:
Using Release()
package main
import (
"fmt"
"test_keyvalues"
)
func createAndRelease() uintptr {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
kv.SetName("ServerConfig")
// Transfer ownership out
handle := kv.Release()
// kv is now invalid, won't clean up
fmt.Println(kv.IsValid()) // false
// No defer needed - we released ownership
return handle
}
func useReleased() {
// We now own the handle and must clean it up manually
rawHandle := createAndRelease()
// ... use rawHandle ...
test_keyvalues.Kv1Destroy(rawHandle) // Manual cleanup required!
}
Go's defer statement provides deterministic cleanup within function scope:
Defer Pattern (Recommended)
package main
import "test_keyvalues"
func processConfig() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
defer kv.Close() // Guaranteed cleanup when function returns
kv.SetName("ServerConfig")
kv.SetString("hostname", "My Server")
// ... use kv ...
// Automatically cleaned up here via defer
}
This is the recommended pattern because:
Guaranteed cleanup even if panics occur
Clear scope of resource lifetime
Deterministic cleanup timing
Idiomatic Go
Best Practice: Always use defer obj.Close() immediately after creating an owned object. This ensures cleanup even if the function panics or returns early.
Plugify uses runtime.Cleanup to register finalizers that cleanup resources during garbage collection. However, this is non-deterministic and should not be relied upon:
Finalizer Fallback
package main
import "test_keyvalues"
func processConfig() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
kv.SetName("ServerConfig")
// ... use kv ...
// NO defer - relies on finalization (BAD!)
}
// kv will eventually be cleaned up by finalizer
// but timing is unpredictable - DO NOT RELY ON THIS!
Important - Plugin Lifecycle: Finalizers are called by Go's garbage collector when objects are collected. However, you must avoid situations where finalizers are called after your plugin is unloaded. If you store objects globally, you are responsible for cleaning them up in PluginEnd(), otherwise behavior is undefined and may cause crashes.
Note: Finalizer timing is non-deterministic and depends on GC cycles. Always use defer for predictable cleanup.
If a class doesn't have a destructor defined in the manifest, it acts as a simple wrapper without automatic cleanup:
Wrapper Without Destructor
package main
import "test_keyvalues"
func useWrapper() {
// Struct with no destructor - just a convenience wrapper
wrapper := test_keyvalues.NewSomeWrapper()
// Still has utility methods
if wrapper.IsValid() {
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 and you should not use the object afterward:
Ownership Transfer
package main
import "test_keyvalues"
func transferOwnership() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
child := test_keyvalues.NewKeyValuesKv1Create("Child")
// DON'T defer child.Close() - 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
package main
import "test_keyvalues"
func properOwnership() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
child := test_keyvalues.NewKeyValuesKv1Create("Child")
// Transfer ownership - child.Release() called internally
parent.AddSubKey(child)
// child.IsValid() is now false
// Only use through parent
found, err := parent.FindKey("Child")
if err != nil {
panic(err)
}
_ = found
}
When a method returns a new resource with ownership:
Return Ownership
package main
import "test_keyvalues"
func returnOwnership() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
// FindKey returns a NEW KeyValues that we own
child, err := parent.FindKey("Settings")
if err != nil {
panic(err)
}
defer child.Close() // We own it, must clean up
if child.IsValid() {
child.SetName("UpdatedSettings")
// We're responsible for child's lifecycle
}
}
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
package main
import "test_keyvalues"
func nonOwningRef() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
// GetFirstSubKey returns a reference, parent still owns it
childRef, err := parent.GetFirstSubKey()
if err != nil {
panic(err)
}
if childRef.IsValid() {
// Use the reference, but DON'T close or release it
name, _ := childRef.GetName()
_ = name
// childRef will be cleaned up by parent
}
}
Important: With non-owning references, the returned object is still valid (has a handle), but you don't own it. Closing the parent will invalidate these references.
When using classes in plugins, you must be careful about object cleanup during plugin unload. If objects are still alive when your plugin is unloaded, their finalizers may be called after the plugin's code is no longer in memory, leading to undefined behavior or crashes.
If you store class instances in global variables or package-level variables, you must explicitly clean them up in your plugin's PluginEnd() function:
plugin.go
package main
import (
"github.com/untrustedmodders/go-plugify"
"test_keyvalues"
)
// Global object - dangerous if not cleaned up!
var gConfig *test_keyvalues.KeyValues
func PluginStart() {
// Create global configuration
gConfig = test_keyvalues.NewKeyValuesKv1Create("GlobalConfig")
gConfig.SetName("ServerSettings")
plugify.Log("Plugin started with global config")
}
func PluginEnd() {
// CRITICAL: Clean up global objects before plugin unload
if gConfig != nil {
gConfig.Close()
gConfig = nil
}
plugify.Log("Plugin ended, global config cleaned up")
}
Critical: Failing to clean up global objects in PluginEnd() may cause finalizers to run after your plugin is unloaded, resulting in crashes or undefined behavior. Always explicitly clean up global resources!
func onCommand(args []string) {
// Local object with defer - automatically cleaned up
kv := test_keyvalues.NewKeyValuesKv1Create("TempConfig")
defer kv.Close()
kv.SetName("CommandConfig")
// ... use kv ...
// Automatically destroyed when function returns - safe!
}
✅ Safe: Function-local variables
Safe Pattern
func processData() {
kv := test_keyvalues.NewKeyValuesKv1Create("TempConfig")
defer kv.Close()
kv.SetName("CommandConfig")
// ... use kv ...
// Cleaned up by defer - safe!
}
✅ Safe: Package variable with cleanup
Safe Pattern
package main
var pluginConfig *test_keyvalues.KeyValues
func PluginStart() {
pluginConfig = test_keyvalues.NewKeyValuesKv1Create("PluginConfig")
pluginConfig.SetName("Settings")
}
func PluginEnd() {
// Clean up package variables
if pluginConfig != nil {
pluginConfig.Close()
pluginConfig = nil
}
}
❌ Unsafe: Global without cleanup
Unsafe Pattern
package main
import "test_keyvalues"
// DANGEROUS: Global object
var gConfig = test_keyvalues.NewKeyValuesKv1Create("GlobalConfig")
func PluginStart() {
gConfig.SetName("ServerSettings")
}
func PluginEnd() {
// MISSING: No cleanup!
// gConfig finalizer will run after plugin unload - CRASH!
}
❌ Unsafe: Package-level map without cleanup
Unsafe Pattern
package main
import "test_keyvalues"
// DANGEROUS: Package-level cache
var kvCache = make(map[string]*test_keyvalues.KeyValues)
func cacheConfig(name string) {
kvCache[name] = test_keyvalues.NewKeyValuesKv1Create(name)
}
func PluginEnd() {
// MISSING: No cache cleanup!
// Cached objects will finalize after plugin unload - CRASH!
}
package main
import "test_keyvalues"
func accidentalCopy() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
defer kv.Close()
// This will be caught by go vet!
// kvCopy := *kv // ERROR: copies lock value
// Use pointers instead
kvPtr := kv // OK
_ = kvPtr
}