Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Lua.
Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая автоматическое управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.
Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:
Без классов (стиль C)
local s2sdk = require("s2sdk")
-- Создать дескриптор KeyValues вручную
local kv_handle = s2sdk.Kv1Create("MyConfig")
-- Установить свойства, используя дескриптор
s2sdk.Kv1SetName(kv_handle, "ServerSettings")
-- Найти подключ
local subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players")
-- Легко забыть очистку!
s2sdk.Kv1Destroy(subkey_handle)
s2sdk.Kv1Destroy(kv_handle)
С классами тот же код становится намного чище и безопаснее:
С классами (стиль ООП)
local s2sdk = require("s2sdk")
-- Создать с использованием конструктора класса (Lua 5.4+ с <close>)
local kv <close> = s2sdk.KeyValues.new("MyConfig")
-- Использовать интуитивные методы
kv:SetName("ServerSettings")
-- Найти подключ - возвращает экземпляр KeyValues
local subkey <close> = kv:FindKey("Players")
-- Автоматическая очистка при выходе из области видимости!
Преимущества:
Более чистый синтаксис - Методы вместо функций с явными дескрипторами
Автоматическое управление ресурсами - Нет необходимости вручную вызывать функции уничтожения
Очистка на основе области видимости - <close> в Lua 5.4 обеспечивает детерминированную очистку ресурсов
Резервная очистка - Финализатор __gc в качестве страховки для старых версий Lua
Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
Идиоматичный API для Lua - Ощущается естественным для разработчиков на Lua
Когда вы определяете классы в своем манифесте, Plugify генерирует обертки классов Lua с несколькими встроенными методами:
Сгенерированный класс (концептуально)
local KeyValues = {}
KeyValues.__type = "KeyValues"
KeyValues.__index = KeyValues
-- Конструктор
function KeyValues.new(...)
local self = setmetatable({}, KeyValues)
-- Сначала инициализировать в недопустимое состояние
self._handle = 0 -- invalid_value
self._owned = Ownership.BORROWED
local args = {...}
-- Конструкция прямого дескриптора
-- Паттерн: KeyValues.new(handle_value, Ownership.OWNED)
if #args >= 2 and Ownership.is(args[2]) then
self._handle = args[1]
self._owned = args[2]
return self
end
-- Режим вызова конструктора
local success, result = pcall(_plugin.Kv1Create, table.unpack(args))
if success then
self._handle = result
self._owned = Ownership.OWNED
return self
else
error(result)
end
end
-- Метод close
function KeyValues:close()
if not self._handle then
return
end
if self._handle ~= 0 and self._owned == Ownership.OWNED then
_plugin.Kv1Destroy(self._handle)
end
self._handle = 0
self._owned = Ownership.BORROWED
end
-- Метаметод __close для переменных to-be-closed в Lua 5.4+
KeyValues.__close = function(self)
self:close()
end
-- Метаметод __gc (финализатор сборщика мусора)
KeyValues.__gc = function(self)
self:close()
end
-- Утилитарные методы
function KeyValues:get()
if not self._handle then
return 0
end
return self._handle
end
function KeyValues:release()
if not self._handle then
return 0
end
local tmp = self._handle
self._handle = 0
self._owned = Ownership.BORROWED
return tmp
end
function KeyValues:reset()
self:close()
end
function KeyValues:valid()
if not self._handle then
return false
end
return self._handle ~= 0
end
-- Связанные методы (с автоматической проверкой дескриптора)
function KeyValues:GetName()
if not self._handle or self._handle == 0 then
error("KeyValues handle is closed or not initialized")
end
return _plugin.Kv1GetName(self._handle)
end
function KeyValues:SetName(name)
if not self._handle or self._handle == 0 then
error("KeyValues handle is closed or not initialized")
end
_plugin.Kv1SetName(self._handle, name)
end
function KeyValues:FindKey(keyName)
if not self._handle or self._handle == 0 then
error("KeyValues handle is closed or not initialized")
end
local result = _plugin.Kv1FindKey(self._handle, keyName)
-- Автоматически оборачивает возвращаемое значение на основе retAlias
if result ~= 0 then
return KeyValues.new(result, Ownership.OWNED)
end
return nil
end
function KeyValues:AddSubKey(subKey)
if not self._handle or self._handle == 0 then
error("KeyValues handle is closed or not initialized")
end
-- Автоматически освобождает владение от параметра subKey
local handle = (type(subKey) == "table" and subKey.release) and subKey:release() or subKey
_plugin.Kv1AddSubKey(self._handle, handle)
end
Возвращает true, если дескриптор действителен (не равен invalidValue):
Использование valid()
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
if kv:valid() then
kv:SetName("ServerConfig")
print("Handle is valid")
else
print("Handle is invalid")
end
-- После release дескриптор становится недействительным
local handle = kv:release()
print(kv:valid()) -- false
Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:
Использование get()
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
-- Получить необработанный дескриптор
local raw_handle = kv:get()
print("Handle value: " .. tostring(raw_handle))
-- Передать в функцию, ожидающую необработанный дескриптор
some_c_function(kv:get())
Предупреждение: Будьте осторожны при использовании get(). Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:
Использование release()
local s2sdk = require("s2sdk")
local function create_and_release()
local kv = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- Передать владение наружу
local handle = kv:release()
-- kv теперь недействителен, не будет очищать
print(kv:valid()) -- false
return handle
end
-- Теперь мы владеем дескриптором и должны очистить его вручную
local raw_handle = create_and_release()
-- ... использовать raw_handle ...
s2sdk.Kv1Destroy(raw_handle) -- Требуется ручная очистка!
Используйте release(), когда вам нужно:
Передать владение другой системе
Сохранить дескриптор в долгоживущей структуре данных
Взаимодействовать с кодом в стиле C, который принимает владение
Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:
Использование close()
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- Явно закрыть дескриптор сейчас
kv:close()
-- Дескриптор теперь недействителен
print(kv:valid()) -- false
-- Методы вызовут ошибку
local success, err = pcall(function()
kv:SetName("Test")
end)
if not success then
print("Error: " .. err) -- "KeyValues handle is closed"
end
Метод close():
Идемпотентный - Безопасно вызывать несколько раз
Вызывается автоматически - С помощью __close и __gc
Учитывает владение - Вызывает деструктор только если объект владеет дескриптором
Lua 5.4 представила переменные to-be-closed с использованием аннотации <close>. Это обеспечивает детерминированную очистку, аналогичную RAII в других языках:
Переменные to-be-closed (Lua 5.4+)
local s2sdk = require("s2sdk")
do
local kv <close> = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
kv:SetString("hostname", "My Server")
-- ... использовать kv ...
-- kv:close() автоматически вызывается при выходе из этой области видимости
end
-- Определенно очищается здесь!
Это рекомендуемый паттерн для Lua 5.4+, потому что:
Гарантированная очистка даже при возникновении ошибок
Финализатор сборщика мусора (__gc) вызывается автоматически при сборке мусора объекта:
Очистка сборщиком мусора
local s2sdk = require("s2sdk")
local function process_config()
local kv = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- ... использовать kv ...
-- kv будет уничтожен при сборке мусора
end
process_config()
-- kv в конечном итоге будет очищен GC
Важно - Жизненный цикл плагина: Финализатор __gc вызывается сборщиком мусора Lua при сборке объекта. Однако вы должны избегать ситуаций, когда финализаторы вызываются после выгрузки вашего плагина. Если вы храните объекты глобально, вы несете ответственность за их очистку в PluginEnd(), иначе поведение будет неопределенным и может привести к сбоям.
Примечание: Время работы __gc недетерминировано и зависит от циклов сборки мусора. Для немедленной очистки используйте <close> (Lua 5.4+) или вызывайте close() явно.
Для немедленной очистки ресурсов в любой версии Lua используйте close() явно:
Ручная очистка
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- ... использовать kv ...
-- Явно очистить сейчас
kv:close()
-- Дескриптор уничтожается немедленно
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:
Обертка без деструктора
local s2sdk = require("s2sdk")
-- Класс без деструктора - просто удобная обертка
local wrapper = s2sdk.SomeWrapper.new()
-- Все еще имеет утилитарные методы
if wrapper:valid() then
local handle = wrapper:get()
end
-- Нет автоматической очистки - дескриптор сохраняется
-- Полезно для оберток без состояния или глобальных ресурсов
Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:
Передача владения
local s2sdk = require("s2sdk")
-- Создать родителя и ребенка
local parent = s2sdk.KeyValues.new("Parent")
local child = s2sdk.KeyValues.new("Child")
-- AddSubKey принимает владение child
parent:AddSubKey(child)
-- child теперь принадлежит parent
-- child:valid() может все еще быть true, но не используйте его!
-- parent обработает очистку
В манифесте это определяется как:
Владение в манифесте
{
"name": "AddSubKey",
"method": "Kv1AddSubKey",
"bindSelf": true,
"paramAliases": [
{
"name": "subKey",
"owner": true // Этот параметр принимает владение
}
]
}
Лучшая практика: После передачи владения избегайте использования объекта:
Правильная обработка владения
local s2sdk = require("s2sdk")
local parent = s2sdk.KeyValues.new("Parent")
local child = s2sdk.KeyValues.new("Child")
-- Передать владение
parent:AddSubKey(child)
-- Освободить нашу ссылку, чтобы предотвратить случайное использование
local child_handle = child:release() -- Теперь child:valid() == false
-- Использовать только через parent
local found = parent:FindKey("Child")
local s2sdk = require("s2sdk")
local parent = s2sdk.KeyValues.new("Parent")
-- FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
local child = parent:FindKey("Settings")
if child and child:valid() then
child:SetName("UpdatedSettings")
-- Мы ответственны за жизненный цикл child
-- Он будет автоматически очищен, когда выйдет из области видимости (Lua 5.4+)
-- Или сборщиком мусора (все версии Lua)
end
Когда owner: false, метод возвращает ссылку без передачи владения:
Невладеющая ссылка
local s2sdk = require("s2sdk")
local parent = s2sdk.KeyValues.new("Parent")
-- GetFirstSubKey возвращает ссылку, parent все еще владеет ею
local child_ref = parent:GetFirstSubKey()
if child_ref and child_ref:valid() then
-- Используйте ссылку, но НЕ закрывайте ее
local name = child_ref:GetName()
-- Не вызывайте child_ref:close() или child_ref:release()
-- child_ref будет очищен parent
end
Важно: С невладеющими ссылками возвращаемый объект все еще действителен (имеет дескриптор), но вы не владеете им. Закрытие родителя сделает эти ссылки недействительными.
При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. Финализаторы сборщика мусора Lua (__gc) вызываются при сборке объектов, но если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.
Если вы храните экземпляры классов в глобальных переменных или таблицах уровня модуля, вы должны явно очистить их в функции PluginEnd() вашего плагина:
plugin.lua
-- Глобальный объект - опасен, если не очищен!
local g_config = nil
function PluginStart()
-- Создать глобальную конфигурацию
g_config = s2sdk.KeyValues.new("GlobalConfig")
g_config:SetName("ServerSettings")
print("Plugin started with global config")
end
function PluginEnd()
-- КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
if g_config then
g_config:close()
g_config = nil
end
print("Plugin ended, global config cleaned up")
end
Критично: Неспособность очистить глобальные объекты в PluginEnd() может привести к тому, что финализаторы запустятся после выгрузки вашего плагина, что приведет к сбоям или неопределенному поведению. Всегда явно очищайте глобальные ресурсы!
✅ Безопасно: Локальная область видимости с to-be-closed (Lua 5.4+)
Безопасный паттерн
function OnCommand(args)
-- Локальный объект с <close> - автоматически очищается
local kv <close> = s2sdk.KeyValues.new("TempConfig")
kv:SetName("CommandConfig")
-- ... использовать kv ...
-- Автоматически уничтожается при выходе из области видимости - безопасно!
end
✅ Безопасно: Локальная область видимости с ручной очисткой
Безопасный паттерн
function OnCommand(args)
local kv = s2sdk.KeyValues.new("TempConfig")
kv:SetName("CommandConfig")
-- ... использовать kv ...
kv:close() -- Явная очистка - безопасно!
end
✅ Безопасно: Переменная модуля с очисткой
Безопасный паттерн
local plugin_config = nil
function PluginStart()
plugin_config = s2sdk.KeyValues.new("PluginConfig")
plugin_config:SetName("Settings")
end
function PluginEnd()
-- Очистить переменные модуля
if plugin_config then
plugin_config:close()
plugin_config = nil
end
end
❌ Небезопасно: Глобальная без очистки
Небезопасный паттерн
-- ОПАСНО: Глобальный объект
g_config = s2sdk.KeyValues.new("GlobalConfig")
function PluginStart()
g_config:SetName("ServerSettings")
end
function PluginEnd()
-- ОТСУТСТВУЕТ: Нет очистки!
-- Финализатор g_config запустится после выгрузки плагина - СБОЙ!
end
❌ Небезопасно: Кэш уровня модуля без очистки
Небезопасный паттерн
-- ОПАСНО: Кэш уровня модуля
local kv_cache = {}
function CacheConfig(name)
kv_cache[name] = s2sdk.KeyValues.new(name)
end
function PluginEnd()
-- ОТСУТСТВУЕТ: Нет очистки кэша!
-- Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
end
Вот полный пример, показывающий, как использовать классы для системы конфигурации:
config_manager.lua
local s2sdk = require("s2sdk")
local ConfigManager = {}
ConfigManager.__index = ConfigManager
function ConfigManager.new(config_name)
local self = setmetatable({}, ConfigManager)
self.root = s2sdk.KeyValues.new(config_name)
if not self.root:valid() then
error("Failed to create config: " .. config_name)
end
return self
end
function ConfigManager:close()
if self.root then
self.root:close()
self.root = nil
end
end
-- Поддержка Lua 5.4+
ConfigManager.__close = function(self)
self:close()
end
function ConfigManager:create_section(section_name)
local section = s2sdk.KeyValues.new(section_name)
if not section:valid() then
return false
end
self.root:AddSubKey(section)
return true
end
function ConfigManager:get_section(section_name)
local section = self.root:FindKey(section_name)
if section and section:valid() then
return section
end
return nil
end
function ConfigManager:set_value(section_name, key, value)
local section = self:get_section(section_name)
if section then
section:SetString(key, value)
return true
end
return false
end
function ConfigManager:get_value(section_name, key, default)
default = default or ""
local section = self:get_section(section_name)
if section then
return section:GetString(key, default)
end
return default
end
function ConfigManager:save(filename)
if not self.root:valid() then
return false
end
return self.root:SaveToFile(filename)
end
function ConfigManager:load(filename)
if not self.root:valid() then
return false
end
return self.root:LoadFromFile(filename)
end
-- Использование (Lua 5.4+)
do
local config <close> = ConfigManager.new("ServerConfig")
config:create_section("Server")
config:set_value("Server", "hostname", "My Server")
config:set_value("Server", "maxplayers", "32")
config:create_section("Game")
config:set_value("Game", "mode", "competitive")
config:save("config.kv")
-- Автоматически очищается при выходе из области видимости
end
Используйте <close> для Lua 5.4+ - Используйте переменные to-be-closed для гарантированной очистки
Используйте close() для немедленной очистки - Более явно, чем полагаться на сборку мусора
Очищайте глобальные объекты в PluginEnd() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости, когда возможно, для автоматической очистки
Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
Уважайте владение - Не используйте объекты после передачи владения
Используйте release() экономно - Только когда вам нужен ручной контроль
Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
Не полагайтесь на время __gc - Сборка мусора недетерминирована, используйте <close> или close() для немедленной очистки
Обрабатывайте ошибки - Будьте готовы перехватывать ошибки от методов, вызванных на закрытых дескрипторах
Проверяйте возвращаемые объекты - Методы могут возвращать nil при неудаче
Если конструктор вызывает ошибку, базовая функция создания вернула недопустимый дескриптор:
Сбой конструктора дескриптора
local success, err = pcall(function()
return s2sdk.KeyValues.new("Config")
end)
if not success then
print("Failed to create KeyValues: " .. err)
-- Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию
end
Попытка использовать объект после вызова close() или release() вызовет ошибку:
Использование закрытого дескриптора
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
kv:close()
-- Это вызовет ошибку
local success, err = pcall(function()
kv:SetName("Test")
end)
if not success then
print(err) -- "KeyValues handle is closed"
end
-- Сначала проверьте valid(), чтобы избежать ошибок
if kv:valid() then
kv:SetName("Test")
else
print("Handle is closed!")
end
Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:
Автоматическая проверка
local s2sdk = require("s2sdk")
local kv = s2sdk.KeyValues.new("Config")
local handle = kv:release()
-- Любой вызов метода завершится неудачей
local success, err = pcall(function()
return kv:GetName()
end)
if not success then
print(err) -- "KeyValues handle is closed"
end
Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:
Проблема висячей ссылки
local function get_child_ref()
local parent = s2sdk.KeyValues.new("Parent")
local child = parent:GetFirstSubKey() -- Невладеющая ссылка
return child -- ПЛОХО: parent будет уничтожен!
end
-- Это опасно!
local child_ref = get_child_ref()
-- child_ref теперь указывает на уничтоженную память
-- Лучший подход:
local function get_child_owned()
local parent = s2sdk.KeyValues.new("Parent")
local child = parent:FindKey("Child") -- Возвращает принадлежащий экземпляр
-- Нужно держать parent живым или убедиться, что child используется до очистки parent
return child -- Безопасно, если FindKey возвращает принадлежащий и используется до GC
end
Проверьте глобальные объекты - Убедитесь, что все глобальные экземпляры классов очищены в PluginEnd()
Проверьте таблицы уровня модуля - Очистите все таблицы, содержащие экземпляры классов
Добавьте явную очистку - Используйте close() для всех долгоживущих объектов перед выгрузкой плагина
Принудительная сборка мусора - Вызовите collectgarbage() перед выгрузкой плагина для раннего запуска финализаторов
Исправление сбоя при выгрузке
-- Глобальные объекты, которые вызывали сбои
local g_config = nil
local g_cache = {}
function PluginStart()
g_config = s2sdk.KeyValues.new("Config")
g_cache["main"] = s2sdk.KeyValues.new("Main")
end
function PluginEnd()
-- Очистить глобальные объекты
if g_config then
g_config:close()
g_config = nil
end
-- Очистить кэшированные объекты
for k, v in pairs(g_cache) do
v:close()
end
g_cache = {}
-- Опционально: Принудительный запуск GC для выполнения финализаторов
collectgarbage()
print("All resources cleaned up safely")
end
Полная поддержка переменных to-be-closed <close> (рекомендуется)
Финализатор __gc в качестве резерва
Lua 5.4+
-- Рекомендуется: Используйте <close>
local kv <close> = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- Автоматически очищается при выходе из области видимости
-- Рекомендуется: Явный close
local kv = s2sdk.KeyValues.new("Config")
kv:SetName("ServerConfig")
kv:close() -- Явная очистка
-- Или полагайтесь на __gc (недетерминировано)
local kv2 = s2sdk.KeyValues.new("Config2")
-- Будет очищен в конечном итоге GC