Using Classes

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.

Why Use Classes?

Working with handle-based APIs in the traditional C-style can be verbose and error-prone:

Without Classes (C-style)
package main

import "test_keyvalues"

func processConfig() {
    // Create a KeyValues handle manually
    kvHandle := test_keyvalues.Kv1Create("MyConfig")

    // Set properties using the handle
    test_keyvalues.Kv1SetName(kvHandle, "ServerSettings")

    // Find a subkey
    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)
package main

import "test_keyvalues"

func processConfig() {
    // Create using a constructor
    kv := test_keyvalues.NewKeyValuesKv1Create("MyConfig")
    defer kv.Close()  // Automatic cleanup when function returns

    // Use intuitive methods with error handling
    if err := kv.SetName("ServerSettings"); err != nil {
        panic(err)
    }

    // Find a subkey - returns a KeyValues instance
    subkey, err := kv.FindKey("Players")
    if err != nil {
        panic(err)
    }

    // Automatic cleanup via defer!
}

Benefits:

  • Cleaner syntax - Methods instead of functions with explicit handles
  • Idiomatic Go error handling - Methods return error instead of using exceptions
  • Defer-based cleanup - Use defer for deterministic scope-based resource cleanup
  • Type safety - Better IDE autocomplete and compile-time checks
  • Less error-prone - Harder to forget cleanup or mix up handles
  • Prevents copying - noCopy guard ensures handles aren't accidentally copied
  • Ownership tracking - Explicit ownership semantics prevent double-free bugs

Important Pattern:

How Classes Work

When a plugin defines classes in its manifest, Plugify automatically generates Go struct wrappers that:

  1. Wrap the underlying handle - Store the raw pointer internally as uintptr
  2. Bind methods - Convert function calls to method calls with automatic handle passing
  3. Manage lifecycle - Register with runtime.Cleanup for non-deterministic finalization
  4. Provide utility methods - Get(), Release(), Reset(), IsValid(), Close() for handle management
  5. Track ownership - Use ownership type to prevent double-free bugs
  6. Prevent copying - Include noCopy guard to catch accidental copies
  7. Validate handles - Automatically check handle validity before method calls
  8. Return errors - Use idiomatic Go error handling for all operations

Defining Classes in Your Manifest

To create classes for your plugin, add a classes section to your manifest:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    {
      "name": "Kv1Create",
      "funcName": "Kv1Create",
      "paramTypes": [
        { "name": "setName", "type": "string" }
      ],
      "retType": { "type": "ptr64" }
    },
    {
      "name": "Kv1Destroy",
      "funcName": "Kv1Destroy",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "void" }
    },
    {
      "name": "Kv1GetName",
      "funcName": "Kv1GetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "string" }
    },
    {
      "name": "Kv1SetName",
      "funcName": "Kv1SetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" },
        { "name": "name", "type": "string" }
      ],
      "retType": { "type": "void" }
    }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        {
          "name": "GetName",
          "method": "Kv1GetName",
          "bindSelf": true
        },
        {
          "name": "SetName",
          "method": "Kv1SetName",
          "bindSelf": true
        }
      ]
    }
  ]
}

Key fields explained:

  • name: The Go struct name (PascalCase required)
  • handleType: The type of the underlying handle (usually ptr64 or ptr32)
  • invalidValue: What value represents an invalid handle (usually "0" or "-1")
  • constructors: Array of method names that create instances
  • destructor: Method name that cleans up resources (optional)
  • bindings: Array of methods available on the struct
    • bindSelf: If true, automatically passes the handle as the first parameter

Generated Go Code

When you define classes in your manifest, Plugify generates Go struct wrappers with several built-in features:

Generated Struct (Conceptual)
package test_keyvalues

import (
    "errors"
    "runtime"
)

// noCopy prevents copying via go vet
type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

// ownership indicates whether the instance owns the underlying handle
type ownership bool

const (
    Owned    ownership = true
    Borrowed ownership = false
)

var (
    KeyValuesErrEmptyHandle = errors.New("KeyValues: empty handle")
)

// KeyValues - RAII wrapper for KeyValues handle
type KeyValues struct {
    handle    uintptr
    cleanup   runtime.Cleanup
    ownership ownership
    noCopy    noCopy
}

// NewKeyValuesKv1Create creates a new KeyValues instance
func NewKeyValuesKv1Create(setName string) *KeyValues {
    return newKeyValuesOwned(Kv1Create(setName))
}

// newKeyValuesBorrowed creates a KeyValues from a borrowed handle (internal use)
func newKeyValuesBorrowed(handle uintptr) *KeyValues {
    if handle == 0 {
        return &KeyValues{}
    }
    return &KeyValues{
        handle:    handle,
        ownership: Borrowed,
    }
}

// newKeyValuesOwned creates a KeyValues from an owned handle (internal use)
func newKeyValuesOwned(handle uintptr) *KeyValues {
    if handle == 0 {
        return &KeyValues{}
    }
    w := &KeyValues{
        handle:    handle,
        ownership: Owned,
    }
    w.cleanup = runtime.AddCleanup(w, w.finalize, struct{}{})
    return w
}

// finalize is the finalizer function (like C++ destructor)
func (w *KeyValues) finalize(_ struct{}) {
    if plugify.Plugin.Loaded {
        w.destroy()
    }
}

// destroy cleans up owned handles
func (w *KeyValues) destroy() {
    if w.handle != 0 && w.ownership == Owned {
        Kv1Destroy(w.handle)
    }
}

// nullify resets the handle
func (w *KeyValues) nullify() {
    w.handle = 0
    w.ownership = Borrowed
}

// Close explicitly destroys the handle (like C++ destructor, but manual)
func (w *KeyValues) Close() {
    w.Reset()
}

// Get returns the underlying handle
func (w *KeyValues) Get() uintptr {
    return w.handle
}

// Release releases ownership and returns the handle
func (w *KeyValues) Release() uintptr {
    if w.ownership == Owned {
        w.cleanup.Stop()
    }
    handle := w.handle
    w.nullify()
    return handle
}

// Reset destroys and resets the handle
func (w *KeyValues) Reset() {
    if w.ownership == Owned {
        w.cleanup.Stop()
    }
    w.destroy()
    w.nullify()
}

// IsValid returns true if handle is not nil
func (w *KeyValues) IsValid() bool {
    return w.handle != 0
}

// Bound methods (with automatic handle validation)
// GetName gets the section name of a KeyValues instance
func (w *KeyValues) GetName() (string, error) {
    if w.handle == 0 {
        var zero string
        return zero, KeyValuesErrEmptyHandle
    }
    return Kv1GetName(w.handle), nil
}

// SetName sets the section name of a KeyValues instance
func (w *KeyValues) SetName(name string) error {
    if w.handle == 0 {
        return KeyValuesErrEmptyHandle
    }
    Kv1SetName(w.handle, name)
    return nil
}

// FindKey finds a key by name
func (w *KeyValues) FindKey(keyName string) (*KeyValues, error) {
    if w.handle == 0 {
        var zero *KeyValues
        return zero, KeyValuesErrEmptyHandle
    }
    return newKeyValuesBorrowed(Kv1FindKey(w.handle, keyName)), nil
}

// AddSubKey adds a subkey to this KeyValues instance
func (w *KeyValues) AddSubKey(subKey *KeyValues) error {
    if w.handle == 0 {
        return KeyValuesErrEmptyHandle
    }
    Kv1AddSubKey(w.handle, subKey.Release())
    return nil
}

Built-in Utility Methods

Every generated struct includes several utility methods for handle management:

IsValid() - Check Handle Validity

Returns true if the handle is valid (not equal to invalidValue):

Using IsValid()
package main

import (
    "fmt"
    "test_keyvalues"
)

func checkHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    if kv.IsValid() {
        kv.SetName("ServerConfig")
        fmt.Println("Handle is valid")
    } else {
        fmt.Println("Handle is invalid")
    }

    // After release, handle becomes invalid
    handle := kv.Release()
    fmt.Println(kv.IsValid())  // false

    // Must manually cleanup released handle
    test_keyvalues.Kv1Destroy(handle)
}

Get() - Access Raw Handle

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())
}

Release() - Transfer Ownership

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!
}

Use Release() when you need to:

  • Transfer ownership to another system
  • Store the handle in a long-lived data structure
  • Interface with C-style code that takes ownership

Close() - Manual Cleanup

Explicitly closes the handle and calls the destructor if the object owns it. This is what defer calls:

Using Close()
package main

import (
    "fmt"
    "test_keyvalues"
)

func manualClose() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")

    // Explicitly close the handle now
    kv.Close()

    // Handle is now invalid
    fmt.Println(kv.IsValid())  // false

    // Methods will return error
    if err := kv.SetName("Test"); err != nil {
        fmt.Printf("Error: %v\n", err)  // "KeyValues: empty handle"
    }
}

The Close() method is:

  • Idempotent - Safe to call multiple times
  • Stops finalizer - Prevents double-cleanup by calling cleanup.Stop()
  • Ownership-aware - Only calls destructor if object owns the handle
  • Defer-friendly - Designed to be used with defer

Reset() - Destroy and Reset

Destroys the handle and resets it to invalid. Similar to Close() but called by Close():

Using Reset()
package main

import (
    "fmt"
    "test_keyvalues"
)

func resetHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")

    // Reset destroys and resets
    kv.Reset()

    fmt.Println(kv.IsValid())  // false
}

Resource Management

Go classes generated by Plugify support deterministic cleanup via defer:

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

Finalizers (Non-Deterministic Fallback)

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!

No Destructor Case

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
}

Working with Ownership

Some methods transfer ownership of resources. The manifest specifies this with the owner field in paramAliases and retAlias.

Taking Ownership (Method Parameters)

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
}

In the manifest, this is defined as:

Manifest Ownership
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "subKey",
      "owner": true  // This parameter takes ownership
    }
  ]
}

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
}

Returning Ownership (Return Values)

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
    }
}

In the manifest:

Manifest Return Ownership
{
  "name": "FindKey",
  "method": "Kv1FindKey",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Caller owns the returned object
  }
}

Non-Owning References

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
    }
}

Plugin Lifecycle and Global Variables

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.

Managing Global Variables

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")
}

Safe Patterns

✅ Safe: Local scope with defer

Safe Pattern
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!
}

Cleanup Checklist

Before your plugin unloads (PluginEnd()), ensure:

  1. ✅ All global class instances are explicitly closed or set to nil
  2. ✅ All package-level maps/slices containing class instances are cleared
  3. ✅ No references to class instances remain in any long-lived data structures
  4. ✅ You've called Close() on all owned objects

Complete Example: Configuration System

Here's a complete example showing how to use classes for a configuration system:

config_manager.go
package main

import (
    "errors"
    "test_keyvalues"
)

type ConfigManager struct {
    root *test_keyvalues.KeyValues
}

func NewConfigManager(configName string) (*ConfigManager, error) {
    root := test_keyvalues.NewKeyValuesKv1Create(configName)

    if !root.IsValid() {
        return nil, errors.New("failed to create config: " + configName)
    }

    return &ConfigManager{root: root}, nil
}

func (cm *ConfigManager) Close() {
    if cm.root != nil {
        cm.root.Close()
        cm.root = nil
    }
}

func (cm *ConfigManager) CreateSection(sectionName string) error {
    section := test_keyvalues.NewKeyValuesKv1Create(sectionName)

    if !section.IsValid() {
        return errors.New("failed to create section: " + sectionName)
    }

    return cm.root.AddSubKey(section)
}

func (cm *ConfigManager) GetSection(sectionName string) (*test_keyvalues.KeyValues, error) {
    return cm.root.FindKey(sectionName)
}

func (cm *ConfigManager) SetValue(sectionName, key, value string) error {
    section, err := cm.GetSection(sectionName)
    if err != nil {
        return err
    }
    defer section.Close()

    return section.SetString(key, value)
}

func (cm *ConfigManager) GetValue(sectionName, key, defaultValue string) (string, error) {
    section, err := cm.GetSection(sectionName)
    if err != nil {
        return defaultValue, err
    }
    defer section.Close()

    return section.GetString(key, defaultValue)
}

func (cm *ConfigManager) Save(filename string) error {
    if !cm.root.IsValid() {
        return errors.New("invalid config root")
    }
    return cm.root.SaveToFile(filename)
}

func (cm *ConfigManager) Load(filename string) error {
    if !cm.root.IsValid() {
        return errors.New("invalid config root")
    }
    return cm.root.LoadFromFile(filename)
}

// Usage
func useConfigManager() error {
    config, err := NewConfigManager("ServerConfig")
    if err != nil {
        return err
    }
    defer config.Close()  // Cleanup when done

    config.CreateSection("Server")
    config.SetValue("Server", "hostname", "My Server")
    config.SetValue("Server", "maxplayers", "32")

    config.CreateSection("Game")
    config.SetValue("Game", "mode", "competitive")

    return config.Save("config.kv")
}

Best Practices

  1. Always use defer - Call defer obj.Close() immediately after creating owned objects
  2. Check errors - All methods return errors, always check them
  3. Clean up globals in PluginEnd() - Critical: Explicitly clean up all global or package-level class instances before plugin unload to avoid crashes
  4. Don't defer released objects - If a method takes ownership, don't defer cleanup on the transferred object
  5. Prefer local scope - Keep class instances in local scope with defer when possible
  6. Check IsValid() - Especially after operations that might fail
  7. Respect ownership - Don't use objects after transferring ownership (they're automatically released)
  8. Don't copy - The noCopy guard will catch copies with go vet
  9. Use error returns - Don't panic in library code, return errors instead
  10. Handle nil returns - Methods might return nil on failure
  11. Run go vet - Catches accidental copies and other issues

When NOT to Use Classes

Classes are designed for resources that need lifecycle management. Don't define classes for:

  • Stateless utility functions - Simple functions that don't manage resources
  • Functions that return primitive values - No need to wrap simple getters
  • One-off operations - Operations that don't maintain state
  • Global singletons - Resources that live for the entire program lifetime

For these cases, continue using the regular function-based API.

Troubleshooting

"empty handle" Error

If a method returns an empty handle error, the underlying create function returned an invalid handle:

Handle Constructor Failure
package main

import (
    "fmt"
    "test_keyvalues"
)

func handleFailure() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    if !kv.IsValid() {
        fmt.Println("Failed to create KeyValues")
        return
    }

    // ... use kv ...
}

Using Closed or Released Objects

Attempting to use an object after calling Close() or Release() will return an error:

Using Closed Handle
package main

import (
    "fmt"
    "test_keyvalues"
)

func useClosedHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.Close()

    // This returns an error
    if err := kv.SetName("Test"); err != nil {
        fmt.Printf("Error: %v\n", err)  // "KeyValues: empty handle"
    }

    // Check IsValid() first to avoid errors
    if kv.IsValid() {
        kv.SetName("Test")
    } else {
        fmt.Println("Handle is closed!")
    }
}

All bound methods automatically validate the handle before calling the underlying C function.

Dangling References

Be careful with non-owning references when the owner is destroyed:

Dangling Reference Problem
package main

import "test_keyvalues"

func getChildRef() *test_keyvalues.KeyValues {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    child, _ := parent.GetFirstSubKey()  // Non-owning reference
    return child  // BAD: parent will be destroyed!
}

// This is dangerous!
func useDanglingRef() {
    childRef := getChildRef()
    // childRef now points to destroyed memory
}

// Better approach:
func getChildOwned() *test_keyvalues.KeyValues {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")

    child, err := parent.FindKey("Child")  // Returns owned instance
    if err != nil {
        parent.Close()
        return nil
    }

    // Parent cleanup is deferred - need to handle carefully
    // Best to use within same function scope
    return child
}

Accidental Copies

The noCopy guard prevents accidental copies:

Copy Detection
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
}

Run go vet to catch these issues:

go vet ./...

Crashes on Plugin Unload

If your plugin crashes when unloading:

  1. Check global variables - Ensure all global class instances are cleaned up in PluginEnd()
  2. Check package-level maps/slices - Clear any collections containing class instances
  3. Add explicit cleanup - Use Close() on all long-lived objects before plugin unload
  4. Run with race detector - go build -race can catch some issues
Fix Unload Crash
package main

import (
    "github.com/untrustedmodders/go-plugify"
    "test_keyvalues"
)

// Global objects that caused crashes
var gConfig *test_keyvalues.KeyValues
var gCache = make(map[string]*test_keyvalues.KeyValues)

func PluginStart() {
    gConfig = test_keyvalues.NewKeyValuesKv1Create("Config")
    gCache["main"] = test_keyvalues.NewKeyValuesKv1Create("Main")
}

func PluginEnd() {
    // Clean up global objects
    if gConfig != nil {
        gConfig.Close()
        gConfig = nil
    }

    // Clean up cached objects
    for key, kv := range gCache {
        kv.Close()
        delete(gCache, key)
    }

    plugify.Log("All resources cleaned up safely")
}

See Also

  • Learn how to export your own classes
  • Learn about importing functions
  • Plugin manifest reference