Using Classes

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

Using Classes in D

Classes in Plugify provide a convenient, type-safe, and RAII-compliant way to work with complex objects exported by plugins. Instead of manually managing raw pointers and calling constructor/destructor functions, you can use generated D struct wrappers that handle resource management automatically.

Why Use Classes?

When a plugin exports functions that create and destroy objects (like Kv1Create and Kv1Destroy), you could call these functions directly:

// Manual approach - error-prone
void* kv = test_keyvalues.Kv1Create("Config");
test_keyvalues.Kv1SetName(kv, "ServerConfig");
auto name = test_keyvalues.Kv1GetName(kv);
test_keyvalues.Kv1Destroy(kv);  // Easy to forget!

However, this approach has several problems:

  1. Resource Leaks: If you forget to call Kv1Destroy(), the resource leaks
  2. Exception Unsafety: If an exception is thrown, destructor won't be called
  3. No Type Safety: Raw void* pointers provide no compile-time type checking
  4. Verbose: You must manually pass the handle to every function call
  5. Error-Prone: Easy to use the handle after it's been destroyed

Classes solve all these problems by using D's RAII (Resource Acquisition Is Initialization):

// RAII approach - automatic, safe, exception-safe
auto kv = KeyValues("Config");
kv.SetName("ServerConfig");
auto name = kv.GetName();
// Automatically destroyed when kv goes out of scope!

How Classes Work

The generator analyzes your plugin manifest and creates D struct wrappers for objects that have both constructor and destructor functions.

Generated Class Example

From a plugin manifest with Kv1Create and Kv1Destroy methods, the generator creates:

/**
 * RAII wrapper for KeyValues handle.
 */
struct KeyValues {
    private void* _handle = null;
    private Ownership _ownership = Ownership.Borrowed;

    /// Disable default postblit to prevent accidental copies
    @disable this(this);

    /**
     * Creates a new KeyValues instance
     * Params:
     *   setName = The name to assign to this KeyValues instance
     */
    this(string setName) {
        this(Kv1Create(setName), Ownership.Owned);
    }

    /**
     * Creates a KeyValues from an existing handle
     * Params:
     *   handle = The KeyValues handle
     *   ownership = Whether this wrapper owns the handle
     */
    this(void* handle, Ownership ownership = Ownership.Borrowed) {
        _handle = handle;
        _ownership = ownership;
    }

    ~this() {
        destroy();
    }

    /// Move constructor (called when moving)
    this(ref return scope KeyValues other) {
        _handle = other._handle;
        _ownership = other._ownership;
        other.nullify();
    }

    /// Move assignment
    ref KeyValues opAssign(KeyValues other) return {
        swap(this, other);
        return this;
    }

    /// Get the underlying handle
    @property void* get() const pure nothrow @nogc {
        return _handle;
    }

    /// Release ownership of the handle
    void* release() nothrow @nogc {
        auto handle = _handle;
        nullify();
        return handle;
    }

    /// Reset the handle
    void reset() nothrow {
        destroy();
        nullify();
    }

    /// Swap two KeyValues instances
    void swap(ref KeyValues other) nothrow @nogc {
        import std.algorithm.mutation : swap;
        swap(_handle, other._handle);
        swap(_ownership, other._ownership);
    }

    /// Boolean conversion operator
    bool opCast(T : bool)() const pure nothrow @nogc {
        return _handle !is null;
    }

    /// Comparison operators
    int opCmp(ref const KeyValues other) const pure nothrow @nogc {
        if (_handle < other._handle) return -1;
        if (_handle > other._handle) return 1;
        return 0;
    }

    bool opEquals(ref const KeyValues other) const pure nothrow @nogc {
        return _handle == other._handle;
    }

    /**
     * Gets the section name of a KeyValues instance
     * Returns:
     *   The name of the KeyValues section
     * Throws: Exception if handle is null
     */
    string GetName() {
        enforce(_handle !is null, "KeyValues: Empty handle");
        return Kv1GetName(_handle);
    }

    /**
     * Sets the section name of a KeyValues instance
     * Params:
     *   name = The new name to assign to this KeyValues section
     * Throws: Exception if handle is null
     */
    void SetName(string name) {
        enforce(_handle !is null, "KeyValues: Empty handle");
        Kv1SetName(_handle, name);
    }

    /**
     * Finds a key by name
     * Params:
     *   keyName = The name of the key to find
     * Returns:
     *   Pointer to the found KeyValues subkey, or NULL if not found
     * Throws: Exception if handle is null
     */
    KeyValues FindKey(string keyName) {
        enforce(_handle !is null, "KeyValues: Empty handle");
        return KeyValues(Kv1FindKey(_handle, keyName), Ownership.Borrowed);
    }

    /**
     * Adds a subkey to this KeyValues instance
     * Params:
     *   subKey = Pointer to the KeyValues object to add as a child
     * Throws: Exception if handle is null
     */
    void AddSubKey(ref KeyValues subKey) {
        enforce(_handle !is null, "KeyValues: Empty handle");
        Kv1AddSubKey(_handle, subKey.release());
    }

    private void destroy() const nothrow {
        if (_handle !is null && _ownership == Ownership.Owned) {
            Kv1Destroy(_handle);
        }
    }

    private void nullify() nothrow @nogc {
        _handle = null;
        _ownership = Ownership.Borrowed;
    }
}

Resource Management with RAII

D structs use RAII for automatic resource management. The destructor is called automatically when the struct goes out of scope.

Automatic Cleanup

Resources are automatically destroyed when objects go out of scope:

void processConfig() {
    auto kv = KeyValues("ServerConfig");
    kv.SetName("Production");
    // Resource is automatically destroyed when kv goes out of scope
} // Destructor called here - Kv1Destroy() is invoked automatically

Disabled Postblit

The generated structs disable postblit (@disable this(this)) to prevent accidental copies:

auto kv1 = KeyValues("Config");
// auto kv2 = kv1;  // ERROR: postblit is disabled

// Use move semantics instead
import std.algorithm.mutation : move;
auto kv2 = move(kv1);  // OK: kv1 is now empty

Move Semantics

D supports move semantics through the move constructor:

import std.algorithm.mutation : move;

auto kv1 = KeyValues("Config1");
auto kv2 = move(kv1);  // kv1's resources transferred to kv2
// kv1 is now empty (handle is null)

Ownership Semantics

Owned Resources

When you create an object using the constructor, the struct owns the resource:

auto kv = KeyValues("Config");
// kv owns the resource (ownership = Ownership.Owned)
// Destructor will call Kv1Destroy() when kv goes out of scope

Borrowed Resources

When a method returns a handle that you don't own (marked with "owner": false in manifest):

auto parent = KeyValues("Parent");
auto child = parent.FindKey("ChildKey");
// child is borrowed (ownership = Ownership.Borrowed)
// child's destructor will NOT call Kv1Destroy()
// parent owns the actual child resource

Important: Borrowed objects must not outlive the object they were borrowed from:

// WRONG - Dangling reference
KeyValues child;
{
    auto parent = KeyValues("Parent");
    child = parent.FindKey("Child");
} // parent is destroyed here, taking child with it
// child now points to destroyed memory!

Ownership Transfer

Some methods take ownership of objects (marked with "owner": true in manifest):

auto parent = KeyValues("Parent");
auto child = KeyValues("Child");

parent.AddSubKey(child);
// child.release() is called internally
// child now has null handle and Borrowed ownership
// parent owns the child resource

// WRONG: Can't use child after transfer
// child.SetName("NewName");  // Throws exception: Empty handle

Working with Nullable Types

For optional class instances, check if handle is valid:

KeyValues findConfig(string name) {
    auto parent = KeyValues("Parent");
    auto child = parent.FindKey(name);

    if (child) {  // Uses opCast!bool
        return child;
    }
    return KeyValues.init;  // Return empty struct
}

auto config = findConfig("MyConfig");
if (config) {
    writeln("Found: ", config.GetName());
} else {
    writeln("Not found");
}

Complete Example

Here's a complete example demonstrating all concepts:

import std.stdio;
import test_keyvalues;

void onPluginStart() {
    // Create owned object
    auto rootConfig = KeyValues("ServerConfig");
    rootConfig.SetName("Production");

    // Create subkeys and transfer ownership
    {
        auto database = KeyValues("Database");
        database.SetName("PostgreSQL");
        rootConfig.AddSubKey(database);
        // database is now empty - ownership transferred
    }

    {
        auto caching = KeyValues("Caching");
        caching.SetName("Redis");
        rootConfig.AddSubKey(caching);
    }

    // Find returns borrowed reference
    auto dbConfig = rootConfig.FindKey("Database");
    if (dbConfig) {
        writeln("Database: ", dbConfig.GetName());
        // dbConfig is borrowed - rootConfig still owns it
    }

    // rootConfig automatically destroyed when function ends
}

Best Practices

  1. Let D manage lifetimes: Use RAII and let destructors handle cleanup
    void process() {
        auto kv = KeyValues("Config");
        // Use kv...
    } // Automatically destroyed
    
  2. Use ref for parameters: Avoid unnecessary moves
    void readConfig(ref const KeyValues kv) {
        auto name = kv.GetName();
    }
    
  3. Check validity before use: Especially with borrowed objects
    auto child = parent.FindKey("Child");
    if (child) {
        child.SetName("NewName");
    }
    
  4. Use move() for transfers: Make ownership transfer explicit
    import std.algorithm.mutation : move;
    
    auto kv1 = KeyValues("Config");
    auto kv2 = move(kv1);
    // kv1 is now empty
    
  5. Don't mix ownership models: Stick to RAII
    // Good
    auto kv = KeyValues("Config");
    
    // Avoid unless necessary
    auto handle = kv.release();
    auto kv2 = KeyValues(handle, Ownership.Owned);
    

Advanced Features

Swap

Swap two KeyValues instances efficiently:

auto kv1 = KeyValues("Config1");
auto kv2 = KeyValues("Config2");

kv1.swap(kv2);
// kv1 now has "Config2", kv2 has "Config1"

Reset

Reset a KeyValues instance (destroys and nullifies):

auto kv = KeyValues("Config");
kv.SetName("NewName");

kv.reset();
// kv now has null handle

Release

Release ownership without destroying:

auto kv = KeyValues("Config");
void* handle = kv.release();
// kv now has null handle, but resource is not destroyed
// You're responsible for calling Kv1Destroy(handle) manually

Comparison and Sorting

The generated structs support comparison:

auto kv1 = KeyValues("Config1");
auto kv2 = KeyValues("Config2");

if (kv1 == kv2) {
    writeln("Same handle");
}

if (kv1 < kv2) {
    writeln("kv1 handle is less than kv2 handle");
}

// Can be used in sorted containers
import std.container : redBlackTree;
auto tree = redBlackTree(kv1, kv2);

Exception Safety

D's RAII ensures exception safety:

void processConfig() {
    auto kv = KeyValues("Config");
    kv.SetName("Production");

    // If an exception is thrown here...
    throw new Exception("Something went wrong!");

    // ...the destructor is still called
} // Destructor called during stack unwinding

Troubleshooting

Empty Handle Exception

Problem: Exception with "KeyValues: Empty handle" message.

Cause: Using a struct with null handle or after ownership transfer.

Solution: Check validity before use:

auto child = parent.FindKey("Child");
if (child) {
    child.SetName("NewName");
}

Postblit Disabled Error

Problem: Compiler error "postblit is disabled".

Cause: Trying to copy the struct.

Solution: Use move semantics:

import std.algorithm.mutation : move;

auto kv1 = KeyValues("Config");
auto kv2 = move(kv1);  // Correct

Use After Transfer

Problem: Exception after transferring ownership.

Cause: Using a struct after calling a method that transfers ownership.

Solution: Don't use structs after ownership transfer:

auto child = KeyValues("Child");
parent.AddSubKey(child);
// Don't use child here - ownership was transferred

Attributes and Qualifiers

The generated structs use D's attribute system:

  • @disable this(this): Prevents copying
  • @property: Property accessor for get()
  • pure: Function has no side effects
  • nothrow: Function doesn't throw exceptions
  • @nogc: Function doesn't allocate GC memory
  • const: Function doesn't modify the object
  • ref return scope: Move constructor attributes

These attributes enable the compiler to perform better optimizations and enforce safety.

Conclusion

D's class wrappers provide a safe, efficient, and idiomatic way to work with Plugify classes. The combination of RAII, move semantics, and D's powerful attribute system ensures resource safety while maintaining performance. By following D's ownership principles and the best practices in this guide, you can build robust plugins with confidence.