Learn how to use class wrappers for cleaner, object-oriented plugin APIs in JavaScript.
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 class-based interfaces.
Working with handle-based APIs in the traditional C-style can be verbose and error-prone:
Without Classes (C-style)
import * as example_plugin from ':example_plugin';
// Create a KeyValues handle manually
const kv_handle = example_plugin.Kv1Create("MyConfig");
// Set properties using the handle
example_plugin.Kv1SetName(kv_handle, "ServerSettings");
// Find a subkey
const subkey_handle = example_plugin.Kv1FindKey(kv_handle, "Players");
// Easy to forget cleanup!
example_plugin.Kv1Destroy(kv_handle);
With classes, the same code becomes much cleaner:
With Classes (OOP-style)
import * as example_plugin from ':example_plugin';
// Create using a class constructor
const kv = new example_plugin.KeyValues("MyConfig");
// Use intuitive methods
kv.SetName("ServerSettings");
// Find a subkey - returns a borrowed KeyValues, or null if there is none
const subkey = kv.FindKey("Players");
// Must manually clean up!
kv.close();
Benefits:
Cleaner syntax - Methods instead of functions with explicit handles
Type safety - Better IDE autocomplete with TypeScript definitions
Less error-prone - Harder to mix up handles
Automatic validation - Methods check handle validity before calling
JavaScript-idiomatic API - Feels natural to JavaScript developers
Important Limitation:
No Deterministic Cleanup: Unlike Python or Lua, JavaScript does not have deterministic resource cleanup. You must call close() explicitly or rely on non-deterministic garbage collection finalization.
When a plugin defines classes in its manifest, the JavaScript language module builds wrapper classes for them in the plugin's synthetic module (':example_plugin'). The wrappers:
Wrap the underlying handle - Store the raw pointer internally
Bind methods - Convert function calls to method calls with automatic handle passing
Manage lifecycle - Register with FinalizationRegistry for eventual cleanup
Provide utility methods - get(), release(), valid(), close(), reset() for handle management
Validate handles - Automatically check handle validity before method calls
Provide type hints - Generate .d.ts TypeScript definition files for IDE support
Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:
Using get()
import * as example_plugin from ':example_plugin';
const kv = new example_plugin.KeyValues("Config");
// Get the raw handle
const raw_handle = kv.get();
console.log(`Handle value: ${raw_handle}`);
// Pass to a function expecting a raw handle
example_plugin.Kv1GetName(kv.get());
Warning: Be careful when using get(). The returned handle is still owned by the class instance and will be destroyed when the instance is cleaned up.
Releases ownership of the handle and returns it. After calling release(), the class instance becomes invalid and will not call the destructor:
Using release()
import * as example_plugin from ':example_plugin';
function create_and_release() {
const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");
// Transfer ownership out
const handle = kv.release();
// kv is now invalid, won't clean up
console.log(kv.valid()); // false
return handle;
}
// We now own the handle and must clean it up manually
const raw_handle = create_and_release();
// ... use raw_handle ...
example_plugin.Kv1Destroy(raw_handle); // Manual cleanup required!
Explicitly closes the handle and calls the destructor if the object owns it:
Using close()
import * as example_plugin from ':example_plugin';
const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");
// Explicitly close the handle now
kv.close();
// Handle is now invalid
console.log(kv.valid()); // false
// Methods will throw error
try {
kv.SetName("Test");
} catch (e) {
console.error(`Error: ${e.message}`); // "KeyValues handle is closed"
}
The close() method is:
Idempotent - Safe to call multiple times
Unregisters from finalizer - Prevents double-cleanup
Ownership-aware - Only calls destructor if object owns the handle
Required for immediate cleanup - JavaScript has no deterministic destructors
Critical: Always call close() explicitly in JavaScript. Do not rely on garbage collection for timely resource cleanup.
import * as example_plugin from ':example_plugin';
const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");
// Reset is equivalent to close
kv.reset();
console.log(kv.valid()); // false
JavaScript has no deterministic resource cleanup mechanism. You must explicitly call close():
Manual Cleanup
import * as example_plugin from ':example_plugin';
function processConfig() {
const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");
try {
// ... use kv ...
} finally {
// CRITICAL: Always clean up in finally block
kv.close();
}
}
processConfig();
Best practice: Always use try-finally blocks to ensure cleanup:
Try-Finally Pattern
import * as example_plugin from ':example_plugin';
function processConfig() {
const kv = new example_plugin.KeyValues("Config");
try {
kv.SetName("ServerConfig");
kv.SetString("hostname", "My Server");
// ... use kv ...
} finally {
kv.close(); // Guaranteed cleanup even if exceptions occur
}
}
Plugify uses JavaScript's FinalizationRegistry internally to cleanup resources when objects are garbage collected. However, this is non-deterministic and should not be relied upon:
Finalization Fallback
import * as example_plugin from ':example_plugin';
function processConfig() {
const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");
// ... use kv ...
// NO close() call - relies on finalization (BAD!)
}
processConfig();
// kv will eventually be cleaned up by FinalizationRegistry
// but timing is unpredictable - DO NOT RELY ON THIS!
Warning: FinalizationRegistry cleanup timing is unpredictable and may occur long after the object is no longer reachable. Always call close() explicitly for timely resource cleanup.
If a class doesn't have a destructor defined in the manifest, it acts as a simple wrapper without automatic cleanup: close() only resets the handle, and the utility methods still work.
When a method takes ownership of a resource, you should transfer it and not use it afterward:
Ownership Transfer
import * as example_plugin from ':example_plugin';
// Create parent and child
const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");
try {
// AddSubKey takes ownership of child
parent.AddSubKey(child);
// AddSubKey released the handle from child: child.valid() is now false
// parent will handle cleanup
} finally {
parent.close();
}
Best practice: After transferring ownership, avoid using the object:
Proper Ownership Handling
import * as example_plugin from ':example_plugin';
const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");
try {
// Transfer ownership
parent.AddSubKey(child);
console.log(`valid: ${child.valid()}`); // valid: false, the wrapper gave up the handle
// Only use through parent
const found = parent.FindKey("Child");
if (found) {
found.close();
}
} finally {
parent.close();
}
When a method returns a new resource with ownership:
Return Ownership
import * as example_plugin from ':example_plugin';
const parent = new example_plugin.KeyValues("Parent");
try {
// MakeCopy returns a NEW KeyValues that we own
const copy = parent.MakeCopy();
if (copy && copy.valid()) {
try {
copy.SetName("ParentCopy");
// We're responsible for copy's lifecycle
} finally {
copy.close(); // Must clean up returned object
}
}
} finally {
parent.close();
}
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
import * as example_plugin from ':example_plugin';
const parent = new example_plugin.KeyValues("Parent");
try {
// GetFirstSubKey returns a reference, parent still owns it
const child_ref = parent.GetFirstSubKey();
if (child_ref && child_ref.valid()) {
// Use the reference, but DON'T close it
const name = child_ref.GetName();
// Don't call child_ref.close() or child_ref.release()
// child_ref will be cleaned up by parent
}
} finally {
parent.close();
}
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 module-level objects, you must explicitly clean them up in your plugin's pluginEnd() function:
plugin.mjs
import { Plugin } from 'plugify';
import * as example_plugin from ':example_plugin';
// Global object - dangerous if not cleaned up!
let g_config = null;
export class MyPlugin extends Plugin {
pluginStart() {
// Create global configuration
g_config = new example_plugin.KeyValues("GlobalConfig");
g_config.SetName("ServerSettings");
console.log("Plugin started with global config");
}
pluginEnd() {
// CRITICAL: Clean up global objects before plugin unload
if (g_config) {
g_config.close();
g_config = null;
}
console.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!
export class MyPlugin extends Plugin {
pluginStart() {
this.config = new example_plugin.KeyValues("PluginConfig");
this.config.SetName("Settings");
}
pluginEnd() {
// Clean up instance variables
if (this.config) {
this.config.close();
this.config = null;
}
}
}
❌ Unsafe: Global without cleanup
Unsafe Pattern
import * as example_plugin from ':example_plugin';
// DANGEROUS: Global object
const g_config = new example_plugin.KeyValues("GlobalConfig");
export class MyPlugin extends Plugin {
pluginStart() {
g_config.SetName("ServerSettings");
}
pluginEnd() {
// MISSING: No cleanup!
// g_config finalizer will run after plugin unload - CRASH!
}
}
❌ Unsafe: Module-level cache without cleanup
Unsafe Pattern
import * as example_plugin from ':example_plugin';
// DANGEROUS: Module-level cache
const kv_cache = new Map();
export class MyPlugin extends Plugin {
cacheConfig(name) {
kv_cache.set(name, new example_plugin.KeyValues(name));
}
pluginEnd() {
// MISSING: No cache cleanup!
// Cached objects will finalize after plugin unload - CRASH!
}
}
Always use try-finally - Wrap resource usage in try-finally blocks for guaranteed cleanup
Call close() explicitly - JavaScript has no deterministic destructors, manual cleanup is required
Clean up global objects in pluginEnd() - Critical: Explicitly clean up all global or module-level class instances before plugin unload to avoid crashes
Don't rely on FinalizationRegistry - Finalization timing is unpredictable, always use close()
Check valid() for safety - Especially after operations that might fail
Respect ownership - Don't use objects after transferring ownership
Use release() sparingly - Only when you need manual control
Avoid mixing styles - Prefer class API over raw C-style functions
Handle errors - Be prepared to catch errors from methods called on closed handles
Check returned objects - Methods might return null on failure
Use TypeScript - Type definitions help catch errors at development time
If the arguments fit none of the class constructors, the constructor throws an Error that starts with "No constructor matched the arguments". If the create function returns the invalid value, the object is created but valid() returns false:
Handle Constructor Failure
import * as example_plugin from ':example_plugin';
try {
const kv = new example_plugin.KeyValues("Config");
// ... use kv ...
kv.close();
} catch (e) {
console.error(`Failed to create KeyValues: ${e.message}`);
// Handle error - maybe retry or use default config
}
Attempting to use an object after calling close() or release() will throw an error:
Using Closed Handle
import * as example_plugin from ':example_plugin';
const kv = new example_plugin.KeyValues("Config");
kv.close();
// This throws an error
try {
kv.SetName("Test");
} catch (e) {
console.error(e.message); // "KeyValues handle is closed"
}
// Check valid() first to avoid exceptions
if (kv.valid()) {
kv.SetName("Test");
} else {
console.log("Handle is closed!");
}
All bound methods automatically validate the handle before calling the underlying C function:
Automatic Validation
import * as example_plugin from ':example_plugin';
const kv = new example_plugin.KeyValues("Config");
const handle = kv.release();
// Any method call will fail
try {
const name = kv.GetName();
} catch (e) {
console.error(e.message); // "KeyValues handle is closed"
}
Check global objects - Ensure all global class instances are cleaned up in pluginEnd()
Check module-level collections - Clear any Map, Set, or Array containing class instances
Check instance variables - Clean up class instance variables in pluginEnd()
Add explicit cleanup - Use close() on all long-lived objects before plugin unload
Fix Unload Crash
import * as example_plugin from ':example_plugin';
// Global objects that caused crashes
let g_config = null;
const g_cache = new Map();
export class MyPlugin extends Plugin {
pluginStart() {
g_config = new example_plugin.KeyValues("Config");
g_cache.set("main", new example_plugin.KeyValues("Main"));
}
pluginEnd() {
// Clean up global objects
if (g_config) {
g_config.close();
g_config = null;
}
// Clean up cached objects
for (const [key, kv] of g_cache) {
kv.close();
}
g_cache.clear();
console.log("All resources cleaned up safely");
}
}