Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в JavaScript.

Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
import * as example_plugin from ':example_plugin';

// Создать дескриптор KeyValues вручную
const kv_handle = example_plugin.Kv1Create("MyConfig");

// Установить свойства, используя дескриптор
example_plugin.Kv1SetName(kv_handle, "ServerSettings");

// Найти подключ
const subkey_handle = example_plugin.Kv1FindKey(kv_handle, "Players");

// Легко забыть очистку!
example_plugin.Kv1Destroy(kv_handle);

С классами тот же код становится намного чище:

С классами (стиль ООП)
import * as example_plugin from ':example_plugin';

// Создать с использованием конструктора класса
const kv = new example_plugin.KeyValues("MyConfig");

// Использовать интуитивные методы
kv.SetName("ServerSettings");

// Найти подключ - возвращает заимствованный KeyValues или null, если его нет
const subkey = kv.FindKey("Players");

// Должны вручную очистить!
kv.close();

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Безопасность типов - Лучшее автодополнение в IDE с определениями TypeScript
  • Меньше ошибок - Труднее перепутать дескрипторы
  • Автоматическая валидация - Методы проверяют действительность дескриптора перед вызовом
  • Идиоматичный API для JavaScript - Ощущается естественным для разработчиков на JavaScript

Важное ограничение:

Как работают классы

Когда плагин определяет классы в своем манифесте, языковой модуль JavaScript создаёт для них классы-обертки в синтетическом модуле плагина (':example_plugin'). Обертки:

  1. Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
  2. Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
  3. Управляют жизненным циклом - Регистрируются в FinalizationRegistry для возможной очистки
  4. Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
  5. Валидируют дескрипторы - Автоматически проверяют действительность дескриптора перед вызовами методов
  6. Предоставляют подсказки типов - Генерируют .d.ts файлы определений TypeScript для поддержки IDE

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

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" } },
    { "name": "Kv1FindKey", "funcName": "Kv1FindKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetFirstSubKey", "funcName": "Kv1GetFirstSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1AddSubKey", "funcName": "Kv1AddSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "subKey", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1MakeCopy", "funcName": "Kv1MakeCopy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetString", "funcName": "Kv1GetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "defaultValue", "type": "string" }], "retType": { "type": "string" } },
    { "name": "Kv1SetString", "funcName": "Kv1SetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "value", "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 },
        { "name": "FindKey", "method": "Kv1FindKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "GetFirstSubKey", "method": "Kv1GetFirstSubKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "AddSubKey", "method": "Kv1AddSubKey", "bindSelf": true, "paramAliases": [{ "name": "KeyValues", "owner": true }] },
        { "name": "MakeCopy", "method": "Kv1MakeCopy", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": true } },
        { "name": "GetString", "method": "Kv1GetString", "bindSelf": true },
        { "name": "SetString", "method": "Kv1SetString", "bindSelf": true }
      ]
    }
  ]
}

Объяснение ключевых полей:

  • name: Имя класса JavaScript (рекомендуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально)
  • bindings: Массив методов, доступных в классе
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код JavaScript

Когда вы определяете классы в своем манифесте, языковой модуль создаёт классы-обертки с несколькими встроенными методами. Концептуально они выглядят так:

Сгенерированный класс (концептуально)
class KeyValues {
    constructor(...args) {
        // Инициализировать в недопустимое состояние
        this._handle = 0;  // invalidValue
        this._owned = Ownership.BORROWED;

        // Конструкция прямого дескриптора
        // Паттерн: new KeyValues(handle_value, Ownership.OWNED)
        if (args.length >= 2 && Ownership.is(args[1])) {
            this._handle = args[0];
            this._owned = args[1];

            // Зарегистрировать для финализации, если принадлежит
            if (this._owned === Ownership.OWNED) {
                finalizerRegistry.register(this, {
                    handle: this._handle,
                    owned: this._owned,
                    destructor: _plugin.Kv1Destroy,
                    invalidValue: 0
                }, this);
            }
            return;
        }

        // Режим вызова конструктора
        try {
            this._handle = _plugin.Kv1Create(...args);
            this._owned = Ownership.OWNED;

            // Зарегистрировать для финализации
            finalizerRegistry.register(this, {
                handle: this._handle,
                owned: this._owned,
                destructor: _plugin.Kv1Destroy,
                invalidValue: 0
            }, this);
        } catch (e) {
            throw e;
        }
    }

    close() {
        if (this._handle !== 0 && this._owned === Ownership.OWNED) {
            _plugin.Kv1Destroy(this._handle);
            // Отменить регистрацию в финализаторе
            finalizerRegistry.unregister(this);
        }
        this._handle = 0;
        this._owned = Ownership.BORROWED;
    }

    // Утилитарные методы
    get() {
        return this._handle;
    }

    release() {
        if (this._owned === Ownership.OWNED) {
            finalizerRegistry.unregister(this);
        }
        const tmp = this._handle;
        this._handle = 0;
        this._owned = Ownership.BORROWED;
        return tmp;
    }

    reset() {
        this.close();
    }

    valid() {
        return this._handle !== 0;
    }

    // Связанные методы (с автоматической валидацией дескриптора)
    GetName() {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        return _plugin.Kv1GetName(this._handle);
    }

    SetName(name) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        _plugin.Kv1SetName(this._handle, name);
    }

    FindKey(keyName) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        const result = _plugin.Kv1FindKey(this._handle, keyName);
        // Автоматически оборачивает возвращаемое значение на основе retAlias
        if (result !== 0) {
            return new KeyValues(result, Ownership.OWNED);
        }
        return null;
    }

    AddSubKey(subKey) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        // Автоматически освобождает владение от параметра subKey
        const handle = (typeof subKey.release === 'function') ? subKey.release() : subKey;
        _plugin.Kv1AddSubKey(this._handle, handle);
    }
}

plugify-gen -lang v8 -classes генерирует соответствующий .d.ts файл определений TypeScript для поддержки IDE:

plugin.d.ts
import { Ownership } from 'plugify';

export class KeyValues {
    constructor(setName: string);
    constructor(handle: bigint, ownership: Ownership);

    close(): void;
    get(): bigint;
    release(): bigint;
    reset(): void;
    valid(): boolean;

    GetName(): string;
    SetName(name: string): void;
    FindKey(keyName: string): KeyValues | null;
    AddSubKey(subKey: KeyValues): void;
}

Встроенные утилитарные методы

Каждый сгенерированный класс включает несколько утилитарных методов для управления дескриптором:

valid() - Проверка действительности дескриптора

Возвращает true, если дескриптор действителен (не равен invalidValue):

Использование valid()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");

if (kv.valid()) {
    kv.SetName("ServerConfig");
    console.log("Handle is valid");
} else {
    console.log("Handle is invalid");
}

// После release дескриптор становится недействительным
const handle = kv.release();
console.log(kv.valid());  // false

get() - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование get()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");

// Получить необработанный дескриптор
const raw_handle = kv.get();
console.log(`Handle value: ${raw_handle}`);

// Передать в функцию, ожидающую необработанный дескриптор
example_plugin.Kv1GetName(kv.get());

release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:

Использование release()
import * as example_plugin from ':example_plugin';

function create_and_release() {
    const kv = new example_plugin.KeyValues("Config");
    kv.SetName("ServerConfig");

    // Передать владение наружу
    const handle = kv.release();

    // kv теперь недействителен, не будет очищать
    console.log(kv.valid());  // false

    return handle;
}

// Теперь мы владеем дескриптором и должны очистить его вручную
const raw_handle = create_and_release();
// ... использовать raw_handle ...
example_plugin.Kv1Destroy(raw_handle);  // Требуется ручная очистка!

Используйте release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

close() - Ручная очистка

Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:

Использование close()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");

// Явно закрыть дескриптор сейчас
kv.close();

// Дескриптор теперь недействителен
console.log(kv.valid());  // false

// Методы вызовут ошибку
try {
    kv.SetName("Test");
} catch (e) {
    console.error(`Error: ${e.message}`);  // "KeyValues handle is closed"
}

Метод close():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Отменяет регистрацию в финализаторе - Предотвращает двойную очистку
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором
  • Требуется для немедленной очистки - JavaScript не имеет детерминированных деструкторов

reset() - Псевдоним для Close

Удобный метод, который вызывает close():

Использование reset()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");

// Reset эквивалентен close
kv.reset();

console.log(kv.valid());  // false

Управление ресурсами

Классы JavaScript, сгенерированные Plugify, требуют ручного управления ресурсами:

Ручная очистка (требуется)

JavaScript не имеет детерминированного механизма очистки ресурсов. Вы должны явно вызывать close():

Ручная очистка
import * as example_plugin from ':example_plugin';

function processConfig() {
    const kv = new example_plugin.KeyValues("Config");
    kv.SetName("ServerConfig");

    try {
        // ... использовать kv ...
    } finally {
        // КРИТИЧНО: Всегда очищайте в блоке finally
        kv.close();
    }
}

processConfig();

Лучшая практика: Всегда используйте блоки try-finally для обеспечения очистки:

Паттерн Try-Finally
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");
        // ... использовать kv ...
    } finally {
        kv.close();  // Гарантированная очистка даже при исключениях
    }
}

FinalizationRegistry (недетерминированный резерв)

Plugify использует JavaScript FinalizationRegistry внутренне для очистки ресурсов при сборке мусора объектов. Однако это недетерминировано и на него не следует полагаться:

Резервная финализация
import * as example_plugin from ':example_plugin';

function processConfig() {
    const kv = new example_plugin.KeyValues("Config");
    kv.SetName("ServerConfig");
    // ... использовать kv ...
    // НЕТ вызова close() - полагается на финализацию (ПЛОХО!)
}

processConfig();
// kv в конечном итоге будет очищен FinalizationRegistry
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки: close() только сбрасывает дескриптор, а утилитарные методы работают как обычно.

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:

Передача владения
import * as example_plugin from ':example_plugin';

// Создать родителя и ребенка
const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");

try {
    // AddSubKey принимает владение child
    parent.AddSubKey(child);

    // AddSubKey забрал дескриптор у child: child.valid() теперь false
    // parent обработает очистку
} finally {
    parent.close();
}

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "KeyValues",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения избегайте использования объекта:

Правильная обработка владения
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");

try {
    // Передать владение
    parent.AddSubKey(child);

    console.log(`valid: ${child.valid()}`);  // valid: false, обертка отдала дескриптор

    // Использовать только через parent
    const found = parent.FindKey("Child");
    if (found) {
        found.close();
    }
} finally {
    parent.close();
}

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.KeyValues("Parent");

try {
    // MakeCopy возвращает НОВЫЙ KeyValues, которым мы владеем
    const copy = parent.MakeCopy();

    if (copy && copy.valid()) {
        try {
            copy.SetName("ParentCopy");
            // Мы ответственны за жизненный цикл copy
        } finally {
            copy.close();  // Должны очистить возвращаемый объект
        }
    }
} finally {
    parent.close();
}

В манифесте:

Возврат владения в манифесте
{
  "name": "MakeCopy",
  "method": "Kv1MakeCopy",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.KeyValues("Parent");

try {
    // GetFirstSubKey возвращает ссылку, parent все еще владеет ею
    const child_ref = parent.GetFirstSubKey();

    if (child_ref && child_ref.valid()) {
        // Используйте ссылку, но НЕ закрывайте ее
        const name = child_ref.GetName();
        // Не вызывайте child_ref.close() или child_ref.release()
        // child_ref будет очищен parent
    }
} finally {
    parent.close();
}

Жизненный цикл плагина и глобальные объекты

При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. Если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.

Управление глобальными объектами

Если вы храните экземпляры классов в глобальных переменных или объектах уровня модуля, вы должны явно очистить их в функции pluginEnd() вашего плагина:

plugin.mjs
import { Plugin } from 'plugify';
import * as example_plugin from ':example_plugin';

// Глобальный объект - опасен, если не очищен!
let g_config = null;

export class MyPlugin extends Plugin {
    pluginStart() {
        // Создать глобальную конфигурацию
        g_config = new example_plugin.KeyValues("GlobalConfig");
        g_config.SetName("ServerSettings");
        console.log("Plugin started with global config");
    }

    pluginEnd() {
        // КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
        if (g_config) {
            g_config.close();
            g_config = null;
        }
        console.log("Plugin ended, global config cleaned up");
    }
}

Безопасные паттерны

✅ Безопасно: Try-finally с очисткой

Безопасный паттерн
export class MyPlugin extends Plugin {
    onCommand(args) {
        const kv = new example_plugin.KeyValues("TempConfig");

        try {
            kv.SetName("CommandConfig");
            // ... использовать kv ...
        } finally {
            kv.close();  // Гарантированная очистка - безопасно!
        }
    }
}

✅ Безопасно: Переменная экземпляра с очисткой

Безопасный паттерн
export class MyPlugin extends Plugin {
    pluginStart() {
        this.config = new example_plugin.KeyValues("PluginConfig");
        this.config.SetName("Settings");
    }

    pluginEnd() {
        // Очистить переменные экземпляра
        if (this.config) {
            this.config.close();
            this.config = null;
        }
    }
}

❌ Небезопасно: Глобальная без очистки

Небезопасный паттерн
import * as example_plugin from ':example_plugin';

// ОПАСНО: Глобальный объект
const g_config = new example_plugin.KeyValues("GlobalConfig");

export class MyPlugin extends Plugin {
    pluginStart() {
        g_config.SetName("ServerSettings");
    }

    pluginEnd() {
        // ОТСУТСТВУЕТ: Нет очистки!
        // Финализатор g_config запустится после выгрузки плагина - СБОЙ!
    }
}

❌ Небезопасно: Кэш уровня модуля без очистки

Небезопасный паттерн
import * as example_plugin from ':example_plugin';

// ОПАСНО: Кэш уровня модуля
const kv_cache = new Map();

export class MyPlugin extends Plugin {
    cacheConfig(name) {
        kv_cache.set(name, new example_plugin.KeyValues(name));
    }

    pluginEnd() {
        // ОТСУТСТВУЕТ: Нет очистки кэша!
        // Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
    }
}

Контрольный список очистки

Перед выгрузкой вашего плагина (pluginEnd()), убедитесь:

  1. ✅ Все глобальные экземпляры классов явно закрыты или установлены в null
  2. ✅ Все коллекции уровня модуля (Map, Set, Array), содержащие экземпляры классов, очищены
  3. ✅ Все переменные экземпляра класса явно очищены
  4. ✅ Не осталось ссылок на экземпляры классов в долгоживущих структурах данных

Полный пример: Система конфигурации

Вот полный пример, показывающий, как использовать классы для системы конфигурации:

config_manager.mjs
import * as example_plugin from ':example_plugin';

export class ConfigManager {
    constructor(config_name) {
        this.root = new example_plugin.KeyValues(config_name);

        if (!this.root.valid()) {
            throw new Error(`Failed to create config: ${config_name}`);
        }
    }

    close() {
        if (this.root) {
            this.root.close();
            this.root = null;
        }
    }

    createSection(section_name) {
        const section = new example_plugin.KeyValues(section_name);

        if (!section.valid()) {
            return false;
        }

        try {
            this.root.AddSubKey(section);
            return true;
        } catch (e) {
            section.close();
            return false;
        }
    }

    getSection(section_name) {
        const section = this.root.FindKey(section_name);

        if (section && section.valid()) {
            return section;
        }
        return null;
    }

    setValue(section_name, key, value) {
        const section = this.getSection(section_name);

        if (section) {
            try {
                section.SetString(key, value);
                return true;
            } finally {
                section.close();
            }
        }
        return false;
    }

    getValue(section_name, key, defaultValue = "") {
        const section = this.getSection(section_name);

        if (section) {
            try {
                return section.GetString(key, defaultValue);
            } finally {
                section.close();
            }
        }
        return defaultValue;
    }

}

// Использование с try-finally
const config = new ConfigManager("ServerConfig");
try {
    config.createSection("Server");
    config.setValue("Server", "hostname", "My Server");
    config.setValue("Server", "maxplayers", "32");

    config.createSection("Game");
    config.setValue("Game", "mode", "competitive");
} finally {
    config.close();  // Должны явно очистить!
}

Лучшие практики

  1. Всегда используйте try-finally - Оборачивайте использование ресурсов в блоки try-finally для гарантированной очистки
  2. Вызывайте close() явно - JavaScript не имеет детерминированных деструкторов, требуется ручная очистка
  3. Очищайте глобальные объекты в pluginEnd() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
  4. Не полагайтесь на FinalizationRegistry - Время финализации непредсказуемо, всегда используйте close()
  5. Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
  6. Уважайте владение - Не используйте объекты после передачи владения
  7. Используйте release() экономно - Только когда вам нужен ручной контроль
  8. Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
  9. Обрабатывайте ошибки - Будьте готовы перехватывать ошибки от методов, вызванных на закрытых дескрипторах
  10. Проверяйте возвращаемые объекты - Методы могут возвращать null при неудаче
  11. Используйте TypeScript - Определения типов помогают находить ошибки во время разработки

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

Ошибка "No constructor matched"

Если аргументы не подходят ни к одному конструктору класса, конструктор выбрасывает Error, сообщение которого начинается с "No constructor matched the arguments". Если функция создания вернула недопустимое значение, объект создаётся, но valid() возвращает false:

Сбой конструктора дескриптора
import * as example_plugin from ':example_plugin';

try {
    const kv = new example_plugin.KeyValues("Config");
    // ... использовать kv ...
    kv.close();
} catch (e) {
    console.error(`Failed to create KeyValues: ${e.message}`);
    // Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию
}

Использование закрытых или освобожденных объектов

Попытка использовать объект после вызова close() или release() вызовет ошибку:

Использование закрытого дескриптора
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");
kv.close();

// Это вызовет ошибку
try {
    kv.SetName("Test");
} catch (e) {
    console.error(e.message);  // "KeyValues handle is closed"
}

// Сначала проверьте valid(), чтобы избежать исключений
if (kv.valid()) {
    kv.SetName("Test");
} else {
    console.log("Handle is closed!");
}

Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:

Автоматическая валидация
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");
const handle = kv.release();

// Любой вызов метода завершится неудачей
try {
    const name = kv.GetName();
} catch (e) {
    console.error(e.message);  // "KeyValues handle is closed"
}

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:

Проблема висячей ссылки
import * as example_plugin from ':example_plugin';

function getChildRef() {
    const parent = new example_plugin.KeyValues("Parent");
    const child = parent.GetFirstSubKey();  // Невладеющая ссылка
    parent.close();
    return child;  // ПЛОХО: parent был уничтожен!
}

// Это опасно!
const child_ref = getChildRef();
// child_ref теперь указывает на уничтоженную память

// Лучший подход:
function getChildOwned() {
    const parent = new example_plugin.KeyValues("Parent");
    try {
        const child = parent.FindKey("Child");  // Невладеющая ссылка
        return child ? child.MakeCopy() : null;  // Копия, которой мы владеем, переживёт parent; её закрывает вызывающий
    } finally {
        parent.close();
    }
}

Сбои при выгрузке плагина

Если ваш плагин падает при выгрузке:

  1. Проверьте глобальные объекты - Убедитесь, что все глобальные экземпляры классов очищены в pluginEnd()
  2. Проверьте коллекции уровня модуля - Очистите все Map, Set или Array, содержащие экземпляры классов
  3. Проверьте переменные экземпляра - Очистите переменные экземпляра класса в pluginEnd()
  4. Добавьте явную очистку - Используйте close() для всех долгоживущих объектов перед выгрузкой плагина
Исправление сбоя при выгрузке
import * as example_plugin from ':example_plugin';

// Глобальные объекты, которые вызывали сбои
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() {
        // Очистить глобальные объекты
        if (g_config) {
            g_config.close();
            g_config = null;
        }

        // Очистить кэшированные объекты
        for (const [key, kv] of g_cache) {
            kv.close();
        }
        g_cache.clear();

        console.log("All resources cleaned up safely");
    }
}

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина