Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в JavaScript.
Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.
Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:
Без классов (стиль C)
import * as s2sdk from ':s2sdk';
// Создать дескриптор KeyValues вручную
const kv_handle = s2sdk.Kv1Create("MyConfig");
// Установить свойства, используя дескриптор
s2sdk.Kv1SetName(kv_handle, "ServerSettings");
// Найти подключ
const subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players");
// Легко забыть очистку!
s2sdk.Kv1Destroy(subkey_handle);
s2sdk.Kv1Destroy(kv_handle);
С классами тот же код становится намного чище:
С классами (стиль ООП)
import * as s2sdk from ':s2sdk';
// Создать с использованием конструктора класса
const kv = new s2sdk.KeyValues("MyConfig");
// Использовать интуитивные методы
kv.SetName("ServerSettings");
// Найти подключ - возвращает экземпляр KeyValues
const subkey = kv.FindKey("Players");
// Должны вручную очистить!
subkey.close();
kv.close();
Преимущества:
Более чистый синтаксис - Методы вместо функций с явными дескрипторами
Безопасность типов - Лучшее автодополнение в IDE с определениями TypeScript
Меньше ошибок - Труднее перепутать дескрипторы
Автоматическая валидация - Методы проверяют действительность дескриптора перед вызовом
Идиоматичный API для JavaScript - Ощущается естественным для разработчиков на JavaScript
Важное ограничение:
Нет детерминированной очистки: В отличие от Python или Lua, JavaScript не имеет детерминированной очистки ресурсов. Вы должны явно вызывать close() или полагаться на недетерминированную финализацию сборщиком мусора.
Возвращает true, если дескриптор действителен (не равен invalidValue):
Использование valid()
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.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 s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
// Получить необработанный дескриптор
const raw_handle = kv.get();
console.log(`Handle value: ${raw_handle}`);
// Передать в функцию, ожидающую необработанный дескриптор
some_c_function(kv.get());
Предупреждение: Будьте осторожны при использовании get(). Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:
Использование release()
import * as s2sdk from ':s2sdk';
function create_and_release() {
const kv = new s2sdk.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 ...
s2sdk.Kv1Destroy(raw_handle); // Требуется ручная очистка!
Используйте release(), когда вам нужно:
Передать владение другой системе
Сохранить дескриптор в долгоживущей структуре данных
Взаимодействовать с кодом в стиле C, который принимает владение
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// Reset эквивалентен close
kv.reset();
console.log(kv.valid()); // false
JavaScript не имеет детерминированного механизма очистки ресурсов. Вы должны явно вызывать close():
Ручная очистка
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
try {
// ... использовать kv ...
} finally {
// КРИТИЧНО: Всегда очищайте в блоке finally
kv.close();
}
}
processConfig();
Лучшая практика: Всегда используйте блоки try-finally для обеспечения очистки:
Паттерн Try-Finally
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
try {
kv.SetName("ServerConfig");
kv.SetString("hostname", "My Server");
// ... использовать kv ...
} finally {
kv.close(); // Гарантированная очистка даже при исключениях
}
}
Plugify использует JavaScript FinalizationRegistry внутренне для очистки ресурсов при сборке мусора объектов. Однако это недетерминировано и на него не следует полагаться:
Резервная финализация
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// ... использовать kv ...
// НЕТ вызова close() - полагается на финализацию (ПЛОХО!)
}
processConfig();
// kv в конечном итоге будет очищен FinalizationRegistry
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!
Предупреждение: Время очистки FinalizationRegistry непредсказуемо и может произойти долго после того, как объект перестанет быть достижимым. Всегда явно вызывайте close() для своевременной очистки ресурсов.
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:
Обертка без деструктора
import * as s2sdk from ':s2sdk';
// Класс без деструктора - просто удобная обертка
const wrapper = new s2sdk.SomeWrapper();
// Все еще имеет утилитарные методы
if (wrapper.valid()) {
const handle = wrapper.get();
}
// Нет автоматической очистки - дескриптор сохраняется
// Полезно для оберток без состояния или глобальных ресурсов
Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:
Передача владения
import * as s2sdk from ':s2sdk';
// Создать родителя и ребенка
const parent = new s2sdk.KeyValues("Parent");
const child = new s2sdk.KeyValues("Child");
try {
// AddSubKey принимает владение child
parent.AddSubKey(child);
// child теперь принадлежит parent
// child.valid() может все еще быть true, но не используйте его!
// parent обработает очистку
} finally {
parent.close();
}
В манифесте это определяется как:
Владение в манифесте
{
"name": "AddSubKey",
"method": "Kv1AddSubKey",
"bindSelf": true,
"paramAliases": [
{
"name": "subKey",
"owner": true // Этот параметр принимает владение
}
]
}
Лучшая практика: После передачи владения избегайте использования объекта:
Правильная обработка владения
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.KeyValues("Parent");
const child = new s2sdk.KeyValues("Child");
try {
// Передать владение
parent.AddSubKey(child);
// Освободить нашу ссылку, чтобы предотвратить случайное использование
child.release(); // Теперь child.valid() === false
// Использовать только через parent
const found = parent.FindKey("Child");
if (found) {
found.close();
}
} finally {
parent.close();
}
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.KeyValues("Parent");
try {
// FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
const child = parent.FindKey("Settings");
if (child && child.valid()) {
try {
child.SetName("UpdatedSettings");
// Мы ответственны за жизненный цикл child
} finally {
child.close(); // Должны очистить возвращаемый объект
}
}
} finally {
parent.close();
}
Когда owner: false, метод возвращает ссылку без передачи владения:
Невладеющая ссылка
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.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 s2sdk from ':s2sdk';
// Глобальный объект - опасен, если не очищен!
let g_config = null;
export class MyPlugin extends Plugin {
pluginStart() {
// Создать глобальную конфигурацию
g_config = new s2sdk.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 - Определения типов помогают находить ошибки во время разработки
Если конструктор вызывает ошибку, базовая функция создания вернула недопустимый дескриптор:
Сбой конструктора дескриптора
import * as s2sdk from ':s2sdk';
try {
const kv = new s2sdk.KeyValues("Config");
// ... использовать kv ...
kv.close();
} catch (e) {
console.error(`Failed to create KeyValues: ${e.message}`);
// Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию
}
Попытка использовать объект после вызова close() или release() вызовет ошибку:
Использование закрытого дескриптора
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.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 s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
const handle = kv.release();
// Любой вызов метода завершится неудачей
try {
const name = kv.GetName();
} catch (e) {
console.error(e.message); // "KeyValues handle is closed"
}
Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:
Проблема висячей ссылки
import * as s2sdk from ':s2sdk';
function getChildRef() {
const parent = new s2sdk.KeyValues("Parent");
const child = parent.GetFirstSubKey(); // Невладеющая ссылка
parent.close();
return child; // ПЛОХО: parent был уничтожен!
}
// Это опасно!
const child_ref = getChildRef();
// child_ref теперь указывает на уничтоженную память
// Лучший подход:
function getChildOwned() {
const parent = new s2sdk.KeyValues("Parent");
try {
const child = parent.FindKey("Child"); // Возвращает принадлежащий экземпляр
// Нужно держать parent живым или убедиться, что child используется правильно
return child; // Вызывающий должен закрыть и parent, и child
} catch (e) {
parent.close();
throw e;
}
}