Using Classes

Learn how to use class wrappers for cleaner, object-oriented plugin APIs in Python.

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.

Why Use Classes?

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

Without Classes (C-style)
from plugify.pps import s2sdk

# Create a KeyValues handle manually
kv_handle = s2sdk.Kv1Create("MyConfig")

# Set properties using the handle
s2sdk.Kv1SetName(kv_handle, "ServerSettings")

# Find a subkey
subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players")

# Easy to forget cleanup!
s2sdk.Kv1Destroy(subkey_handle)
s2sdk.Kv1Destroy(kv_handle)

With classes, the same code becomes much cleaner and safer:

With Classes (OOP-style)
from plugify.pps import s2sdk

# Create using a class constructor
with s2sdk.KeyValues("MyConfig") as kv:
    # Use intuitive methods
    kv.SetName("ServerSettings")

    # Find a subkey - returns a KeyValues instance
    subkey = kv.FindKey("Players")

    # Automatic cleanup when exiting the context!

Benefits:

  • Cleaner syntax - Methods instead of functions with explicit handles
  • Automatic resource management - No need to manually call destroy functions
  • Type safety - Better IDE autocomplete and type checking
  • Less error-prone - Harder to forget cleanup or mix up handles
  • Pythonic API - Feels natural to Python developers

How Classes Work

When a plugin defines classes in its manifest, Plugify automatically generates Python class wrappers that:

  1. Wrap the underlying handle - Store the raw pointer internally
  2. Bind methods - Convert function calls to method calls with automatic handle passing
  3. Manage lifecycle - Automatically call destructor when the object is no longer needed
  4. Provide utility methods - get(), release(), valid(), close(), reset() for handle management
  5. Support context managers - Implement __enter__ and __exit__ for with statements
  6. Provide type hints - Generate .pyi stub files for IDE support

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 Python class name (PascalCase recommended)
  • 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 class
    • bindSelf: If true, automatically passes the handle as the first parameter

Generated Python Code

When you define classes in your manifest, Plugify generates Python wrapper classes with several built-in methods:

Generated Class (Conceptual)
class KeyValues:
    """RAII wrapper for KeyValues handle"""

    def __init__(self, *args, **kwargs):
        """Creates a new KeyValues instance

        Can be called in two ways:
        1. Constructor mode: KeyValues("ConfigName")
        2. Direct handle mode: KeyValues(handle_value, Ownership.OWNED)
        """
        from plugify import Ownership

        # Initialize to invalid state first
        self._handle = 0  # invalid_value
        self._owned = Ownership.BORROWED

        # Direct handle construction
        if len(args) >= 2 and isinstance(args[1], Ownership):
            self._handle = args[0]
            self._owned = args[1]
            return

        # Constructor call mode
        try:
            self._handle = _plugin.Kv1Create(*args, **kwargs)
            self._owned = Ownership.OWNED
        except Exception as e:
            raise e

    def close(self):
        """Close/destroy the handle if owned"""
        if not hasattr(self, '_handle'):
            return

        if self._handle != 0 and self._owned == Ownership.OWNED:
            _plugin.Kv1Destroy(self._handle)
        self._handle = 0
        self._owned = Ownership.BORROWED

    def __del__(self):
        """Automatically called when object is destroyed"""
        self.close()

    def __enter__(self):
        """Context manager entry"""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - ensures cleanup"""
        self.close()
        return False

    # Utility methods
    def get(self) -> int:
        """Returns the underlying handle value"""
        if not hasattr(self, '_handle'):
            return 0
        return self._handle

    def release(self) -> int:
        """Releases ownership and returns the handle"""
        if not hasattr(self, '_handle'):
            return 0
        tmp = self._handle
        self._handle = 0
        self._owned = Ownership.BORROWED
        return tmp

    def reset(self):
        """Reset the handle by closing it"""
        self.close()

    def valid(self) -> bool:
        """Checks if the handle is valid"""
        if not hasattr(self, '_handle'):
            return False
        return self._handle != 0

    # Bound methods (with automatic handle validation)
    def GetName(self) -> str:
        """Gets the section name"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        return _plugin.Kv1GetName(self._handle)

    def SetName(self, name: str) -> None:
        """Sets the section name"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        _plugin.Kv1SetName(self._handle, name)

    def FindKey(self, keyName: str) -> 'KeyValues':
        """Finds a key by name, returns owned KeyValues instance"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        result = _plugin.Kv1FindKey(self._handle, keyName)
        # Automatically wraps return value based on retAlias
        if result != 0:
            return KeyValues(result, Ownership.OWNED)
        return None

    def AddSubKey(self, subKey: 'KeyValues') -> None:
        """Adds a subkey, takes ownership of the parameter"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        # Automatically releases ownership from subKey parameter
        handle = subKey.release() if hasattr(subKey, 'release') else subKey
        _plugin.Kv1AddSubKey(self._handle, handle)

A corresponding .pyi stub file is also generated for IDE support:

plugin.pyi
from typing import Optional
from plugify import Ownership

class KeyValues:
    """RAII wrapper for KeyValues handle"""

    def __init__(self, setName: str) -> None: ...
    def __enter__(self) -> 'KeyValues': ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> bool: ...

    def close(self) -> None: ...
    def get(self) -> int: ...
    def release(self) -> int: ...
    def reset(self) -> None: ...
    def valid(self) -> bool: ...

    def GetName(self) -> str: ...
    def SetName(self, name: str) -> None: ...
    def FindKey(self, keyName: str) -> Optional['KeyValues']: ...
    def AddSubKey(self, subKey: 'KeyValues') -> None: ...

Built-in Utility Methods

Every generated class includes several utility methods for handle management:

valid() - Check Handle Validity

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

Using valid()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")

if kv.valid():
    kv.SetName("ServerConfig")
    print("Handle is valid")
else:
    print("Handle is invalid")

# After release, handle becomes invalid
handle = kv.release()
print(kv.valid())  # False

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()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")

# Get the raw handle
raw_handle = kv.get()
print(f"Handle value: {raw_handle}")

# Pass to a function expecting a raw handle
some_c_function(kv.get())

release() - Transfer Ownership

Releases ownership of the handle and returns it. After calling release(), the class instance becomes invalid and will not call the destructor:

Using release()
from plugify.pps import s2sdk

def create_and_release():
    kv = s2sdk.KeyValues("Config")
    kv.SetName("ServerConfig")

    # Transfer ownership out
    handle = kv.release()

    # kv is now invalid, won't clean up
    print(kv.valid())  # False

    return handle

# We now own the handle and must clean it up manually
raw_handle = create_and_release()
# ... use raw_handle ...
s2sdk.Kv1Destroy(raw_handle)  # 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:

Using close()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")

# Explicitly close the handle now
kv.close()

# Handle is now invalid
print(kv.valid())  # False

# Methods will raise RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(f"Error: {e}")  # "KeyValues handle is closed"

The close() method is:

  • Idempotent - Safe to call multiple times
  • Called automatically - By __del__ and __exit__
  • Ownership-aware - Only calls destructor if object owns the handle

reset() - Alias for Close

Convenience method that calls close():

Using reset()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")

# Reset is equivalent to close
kv.reset()

print(kv.valid())  # False

Resource Management

Python classes generated by Plugify support multiple cleanup patterns:

When a destructor is defined, classes automatically support the with statement via __enter__ and __exit__:

Context Manager
from plugify.pps import s2sdk

with s2sdk.KeyValues("Config") as kv:
    kv.SetName("ServerConfig")
    kv.SetString("hostname", "My Server")
    # ... use kv ...
# Automatically cleaned up when exiting the 'with' block

This is the recommended pattern because:

  • Guaranteed cleanup even if exceptions occur
  • Clear scope of resource lifetime
  • Pythonic and familiar to Python developers

Automatic Cleanup

The destructor (__del__) is called automatically when the object is garbage collected:

Automatic Cleanup
from plugify.pps import s2sdk

def process_config():
    kv = s2sdk.KeyValues("Config")
    kv.SetName("ServerConfig")
    # ... use kv ...
    # kv is automatically destroyed when function returns

process_config()  # kv cleaned up here

Manual Cleanup

For immediate resource cleanup, use close() or delete the object:

Manual Cleanup
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# ... use kv ...

# Option 1: Explicitly close
kv.close()

# Option 2: Delete the object
kv2 = s2sdk.KeyValues("Config2")
del kv2

# Both result in immediate cleanup

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
from plugify.pps import s2sdk

# Class with no destructor - just a convenience wrapper
wrapper = s2sdk.SomeWrapper()

# Still has utility methods
if wrapper.valid():
    handle = wrapper.get()

# 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, you should transfer it and not use it afterward:

Ownership Transfer
from plugify.pps import s2sdk

# Create parent and child
parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")

# AddSubKey takes ownership of child
parent.AddSubKey(child)

# child is now owned by parent
# child.valid() may still be True, but don't use it!
# 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, avoid using the object:

Proper Ownership Handling
from plugify.pps import s2sdk

parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")

# Transfer ownership
parent.AddSubKey(child)

# Release our reference to prevent accidental use
child_handle = child.release()  # Now child.valid() == False

# Only use through parent
found = parent.FindKey("Child")

Returning Ownership (Return Values)

When a method returns a new resource with ownership:

Return Ownership
from plugify.pps import s2sdk

parent = s2sdk.KeyValues("Parent")

# FindKey returns a NEW KeyValues that we own
child = parent.FindKey("Settings")

if child and child.valid():
    child.SetName("UpdatedSettings")
    # We're responsible for child's lifecycle
    # It will be cleaned up automatically when it goes out of scope

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
from plugify.pps import s2sdk

parent = s2sdk.KeyValues("Parent")

# GetFirstSubKey returns a reference, parent still owns it
child_ref = parent.GetFirstSubKey()

if child_ref and child_ref.valid():
    # Use the reference, but DON'T delete it
    name = child_ref.GetName()
    # Don't call del child_ref or use release()
    # child_ref will be cleaned up by parent

Plugin Lifecycle and Global Objects

When using classes in plugins, you must be careful about object cleanup during plugin unload. CPython uses reference counting, which means finalizers (__del__) are deterministic and called immediately when the reference count reaches zero. However, 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 Objects

If you store class instances in global variables or module-level attributes, you must explicitly clean them up in your plugin's pluginEnd() function:

plugin.py
from plugify import PluginEntryPoint
from plugify.pps import s2sdk

# Global object - dangerous if not cleaned up!
g_config = None

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        global g_config
        # Create global configuration
        g_config = s2sdk.KeyValues("GlobalConfig")
        g_config.SetName("ServerSettings")
        print("Plugin started with global config")

    def pluginEnd(self):
        global g_config
        # CRITICAL: Clean up global objects before plugin unload
        if g_config:
            g_config.close()  # or del g_config
            g_config = None
        print("Plugin ended, global config cleaned up")

Safe Patterns

✅ Safe: Local scope with automatic cleanup

Safe Pattern
class MyPlugin(PluginEntryPoint):
    def onCommand(self, args):
        # Local object - automatically cleaned up when function returns
        kv = s2sdk.KeyValues("TempConfig")
        kv.SetName("CommandConfig")
        # ... use kv ...
        # Automatically destroyed here - safe!

✅ Safe: Context manager

Safe Pattern
class MyPlugin(PluginEntryPoint):
    def onCommand(self, args):
        # Context manager ensures cleanup before function returns
        with s2sdk.KeyValues("TempConfig") as kv:
            kv.SetName("CommandConfig")
            # ... use kv ...
        # Definitely cleaned up here - safe!

✅ Safe: Instance variable with cleanup

Safe Pattern
class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        self.config = s2sdk.KeyValues("PluginConfig")
        self.config.SetName("Settings")

    def pluginEnd(self):
        # Clean up instance variables
        if hasattr(self, 'config') and self.config:
            self.config.close()
            self.config = None

❌ Unsafe: Global without cleanup

Unsafe Pattern
from plugify.pps import s2sdk

# DANGEROUS: Global object
g_config = s2sdk.KeyValues("GlobalConfig")

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        g_config.SetName("ServerSettings")

    def pluginEnd(self):
        # MISSING: No cleanup!
        # g_config finalizer will run after plugin unload - CRASH!
        pass

❌ Unsafe: Module-level cache without cleanup

Unsafe Pattern
from plugify.pps import s2sdk

# DANGEROUS: Module-level cache
_kv_cache = {}

class MyPlugin(PluginEntryPoint):
    def cacheConfig(self, name):
        _kv_cache[name] = s2sdk.KeyValues(name)

    def pluginEnd(self):
        # MISSING: No cache cleanup!
        # Cached objects will finalize after plugin unload - CRASH!
        pass

Cleanup Checklist

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

  1. ✅ All global class instances are explicitly closed or deleted
  2. ✅ All module-level collections (lists, dicts, sets) containing class instances are cleared
  3. ✅ All class instance variables are explicitly cleaned up
  4. ✅ No references to class instances remain in any long-lived data structures
  5. ✅ Circular references are broken (if any)

Complete Example: Configuration System

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

config_manager.py
from plugify.pps import s2sdk
from typing import Optional

class ConfigManager:
    def __init__(self, config_name: str):
        """Initialize configuration manager"""
        self.root = s2sdk.KeyValues(config_name)

        if not self.root.valid():
            raise RuntimeError(f"Failed to create config: {config_name}")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Cleanup handled by self.root
        return False

    def create_section(self, section_name: str) -> bool:
        """Create a new configuration section"""
        section = s2sdk.KeyValues(section_name)

        if not section.valid():
            return False

        self.root.AddSubKey(section)
        return True

    def get_section(self, section_name: str) -> Optional[s2sdk.KeyValues]:
        """Get a configuration section"""
        section = self.root.FindKey(section_name)

        if section and section.valid():
            return section
        return None

    def set_value(self, section_name: str, key: str, value: str) -> bool:
        """Set a configuration value"""
        section = self.get_section(section_name)

        if section:
            section.SetString(key, value)
            return True
        return False

    def get_value(self, section_name: str, key: str, default: str = "") -> str:
        """Get a configuration value"""
        section = self.get_section(section_name)

        if section:
            return section.GetString(key, default)
        return default

    def save(self, filename: str) -> bool:
        """Save configuration to file"""
        if not self.root.valid():
            return False
        return self.root.SaveToFile(filename)

    def load(self, filename: str) -> bool:
        """Load configuration from file"""
        if not self.root.valid():
            return False
        return self.root.LoadFromFile(filename)

# Usage with context manager
with ConfigManager("ServerConfig") as config:
    config.create_section("Server")
    config.set_value("Server", "hostname", "My Server")
    config.set_value("Server", "maxplayers", "32")

    config.create_section("Game")
    config.set_value("Game", "mode", "competitive")

    config.save("config.kv")

# Resources automatically cleaned up

Advanced Example: Manual Handle Management

Sometimes you need to work with raw handles alongside class instances:

mixed_usage.py
from plugify.pps import s2sdk

def process_with_mixed_api():
    # Create using class
    kv = s2sdk.KeyValues("Config")

    # Use class methods
    kv.SetName("ProcessedConfig")

    # Get raw handle for C-style function
    raw_handle = kv.get()

    # Call legacy C-style function
    result = s2sdk.SomeLegacyFunction(raw_handle)

    # Continue using class methods
    if kv.valid():
        kv.SaveToFile("output.kv")

    # Automatic cleanup still works
    # kv will be destroyed when function returns

def transfer_ownership_example():
    # Create and configure
    kv = s2sdk.KeyValues("Config")
    kv.SetName("TransferTest")

    # Release ownership
    handle = kv.release()

    # kv is now invalid
    assert not kv.valid()

    # We must manually destroy
    s2sdk.Kv1Destroy(handle)

    # Or wrap it again
    kv2 = s2sdk.KeyValues("NewConfig")
    # ... use kv2 ...

Best Practices

  1. Always use context managers - Use with statements for guaranteed cleanup
  2. Use close() for immediate cleanup - More explicit than relying on del or garbage collection
  3. Clean up global objects in pluginEnd() - Critical: Explicitly clean up all global or module-level class instances before plugin unload to avoid crashes
  4. Prefer local scope - Keep class instances in local scope when possible for automatic cleanup
  5. Check valid() for safety - Especially after operations that might fail
  6. Respect ownership - Don't use objects after transferring ownership
  7. Use release() sparingly - Only when you need manual control
  8. Avoid mixing styles - Prefer class API over raw C-style functions
  9. Trust automatic cleanup for local objects - Let __del__ handle cleanup for local objects, but always clean up globals manually
  10. Handle RuntimeError - Be prepared to catch exceptions from methods called on closed handles
  11. Check returned objects - Methods might return None on failure

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

"Failed to create X" Error

If a constructor raises an exception, the underlying create function returned an invalid handle:

Handle Constructor Failure
try:
    kv = s2sdk.KeyValues("Config")
except RuntimeError as e:
    print(f"Failed to create KeyValues: {e}")
    # Handle error - maybe retry or use default config

Using Closed or Released Objects

Attempting to use an object after calling close(), release(), or after destruction will raise a RuntimeError:

Using Closed Handle
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.close()

# This raises RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(e)  # "KeyValues handle is closed"

# Check valid() first to avoid exceptions
if kv.valid():
    kv.SetName("Test")
else:
    print("Handle is closed!")

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

Automatic Validation
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
handle = kv.release()

# Any method call will fail
try:
    name = kv.GetName()
except RuntimeError as e:
    print(e)  # "KeyValues handle is closed"

Dangling References

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

Dangling Reference Problem
def get_child_ref():
    parent = s2sdk.KeyValues("Parent")
    child = parent.GetFirstSubKey()  # Non-owning reference
    return child  # BAD: parent will be destroyed!

# This is dangerous!
child_ref = get_child_ref()
# child_ref now points to destroyed memory

# Better approach:
def get_child_owned():
    parent = s2sdk.KeyValues("Parent")
    child = parent.FindKey("Child")  # Returns owned instance
    # Need to keep parent alive or transfer child ownership
    return parent.FindKey("Child")  # Safe if FindKey returns owned

Memory Leaks

If you're experiencing memory leaks:

  1. Check for circular references - Python's GC handles most cases, but circular refs can delay cleanup
  2. Use context managers - Guaranteed cleanup with with statements
  3. Check release() usage - Make sure released handles are eventually destroyed
  4. Verify ownership - Ensure you're not keeping references to owned objects

Crashes on Plugin Unload

If your plugin crashes when unloading:

  1. Check global objects - Ensure all global class instances are cleaned up in pluginEnd()
  2. Check module-level collections - Clear any lists, dicts, or sets containing class instances
  3. Check instance variables - Clean up class instance variables in pluginEnd()
  4. Add explicit cleanup - Use close() or del on all long-lived objects before plugin unload
Fix Unload Crash
# Global objects that caused crashes
g_config = None
g_cache = {}

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        global g_config, g_cache
        g_config = s2sdk.KeyValues("Config")
        g_cache["main"] = s2sdk.KeyValues("Main")

    def pluginEnd(self):
        global g_config, g_cache

        # Clean up global objects
        if g_config:
            g_config.close()
            g_config = None

        # Clean up cached objects
        for kv in g_cache.values():
            kv.close()
        g_cache.clear()

        print("All resources cleaned up safely")

See Also

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