Узнайте, как использовать обертки классов для более чистых 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
Важное ограничение:
Нет детерминированной очистки: В отличие от Python или Lua, JavaScript не имеет детерминированной очистки ресурсов. Вы должны явно вызывать close() или полагаться на недетерминированную финализацию сборщиком мусора.
Когда плагин определяет классы в своем манифесте, языковой модуль JavaScript создаёт для них классы-обертки в синтетическом модуле плагина (':example_plugin'). Обертки:
Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
Управляют жизненным циклом - Регистрируются в FinalizationRegistry для возможной очистки
Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
Валидируют дескрипторы - Автоматически проверяют действительность дескриптора перед вызовами методов
Предоставляют подсказки типов - Генерируют .d.ts файлы определений TypeScript для поддержки IDE
Когда вы определяете классы в своем манифесте, языковой модуль создаёт классы-обертки с несколькими встроенными методами. Концептуально они выглядят так:
Сгенерированный класс (концептуально)
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:
Возвращает 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
Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле 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());
Предупреждение: Будьте осторожны при использовании get(). Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова 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, который принимает владение
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 не имеет детерминированного механизма очистки ресурсов. Вы должны явно вызывать 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(); // Гарантированная очистка даже при исключениях
}
}
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
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!
Предупреждение: Время очистки FinalizationRegistry непредсказуемо и может произойти долго после того, как объект перестанет быть достижимым. Всегда явно вызывайте close() для своевременной очистки ресурсов.
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки: close() только сбрасывает дескриптор, а утилитарные методы работают как обычно.
Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:
Передача владения
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();
}
Когда 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");
}
}
Критично: Неспособность очистить глобальные объекты в pluginEnd() может привести к тому, что финализаторы запустятся после выгрузки вашего плагина, что приведет к сбоям или неопределенному поведению. Всегда явно очищайте глобальные ресурсы!
Всегда используйте try-finally - Оборачивайте использование ресурсов в блоки try-finally для гарантированной очистки
Вызывайте close() явно - JavaScript не имеет детерминированных деструкторов, требуется ручная очистка
Очищайте глобальные объекты в pluginEnd() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
Не полагайтесь на FinalizationRegistry - Время финализации непредсказуемо, всегда используйте close()
Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
Уважайте владение - Не используйте объекты после передачи владения
Используйте release() экономно - Только когда вам нужен ручной контроль
Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
Обрабатывайте ошибки - Будьте готовы перехватывать ошибки от методов, вызванных на закрытых дескрипторах
Проверяйте возвращаемые объекты - Методы могут возвращать null при неудаче
Используйте TypeScript - Определения типов помогают находить ошибки во время разработки
Если аргументы не подходят ни к одному конструктору класса, конструктор выбрасывает 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"
}