Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Lua.
Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая автоматическое управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.
Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:
Без классов (стиль C)
local test_keyvalues = require("test_keyvalues")
-- Создать дескриптор KeyValues вручную
local kv_handle = test_keyvalues.Kv1Create("MyConfig")
-- Установить свойства, используя дескриптор
test_keyvalues.Kv1SetName(kv_handle, "ServerSettings")
-- Найти подключ
local subkey_handle = test_keyvalues.Kv1FindKey(kv_handle, "Players")
-- Легко забыть очистку!
test_keyvalues.Kv1Destroy(subkey_handle)
test_keyvalues.Kv1Destroy(kv_handle)
С классами тот же код становится намного чище и безопаснее:
С классами (стиль ООП)
local test_keyvalues = require("test_keyvalues")
-- Создать с использованием конструктора класса (Lua 5.4+ с <close>)
local kv <close> = test_keyvalues.KeyValues.new("MyConfig")
-- Использовать интуитивные методы
kv:SetName("ServerSettings")
-- Найти подключ - возвращает экземпляр KeyValues
local subkey <close> = kv:FindKey("Players")
-- Автоматическая очистка при выходе из области видимости!
Преимущества:
Более чистый синтаксис - Методы вместо функций с явными дескрипторами
Автоматическое управление ресурсами - Нет необходимости вручную вызывать функции уничтожения
Очистка на основе области видимости - <close> в Lua 5.4 обеспечивает детерминированную очистку ресурсов
Резервная очистка - Финализатор __gc в качестве страховки для старых версий Lua
Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
Идиоматичный API для Lua - Ощущается естественным для разработчиков на Lua
Когда плагин определяет классы в своём манифесте, модуль Lua создаёт обёртки классов во время выполнения и кладёт их в модуль плагина (require('plugin_name').ClassName); ничего генерировать не нужно. Обёртки:
Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
Управляют жизненным циклом - Автоматически вызывают деструктор, когда объект больше не нужен
Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
Поддерживают очистку на основе области видимости - Реализуют __close для переменных to-be-closed в Lua 5.4+
Предоставляют резервную очистку - Реализуют __gc для финализации сборщиком мусора
Когда вы определяете классы в своем манифесте, 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 test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.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 test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.KeyValues.new("Config")
-- Получить необработанный дескриптор
local raw_handle = kv:get()
print("Handle value: " .. tostring(raw_handle))
-- Передать в функцию, ожидающую необработанный дескриптор
some_c_function(kv:get())
Предупреждение: Будьте осторожны при использовании get(). Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:
Использование release()
local test_keyvalues = require("test_keyvalues")
local function create_and_release()
local kv = test_keyvalues.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 ...
test_keyvalues.Kv1Destroy(raw_handle) -- Требуется ручная очистка!
Используйте release(), когда вам нужно:
Передать владение другой системе
Сохранить дескриптор в долгоживущей структуре данных
Взаимодействовать с кодом в стиле C, который принимает владение
Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:
Использование close()
local test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.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 test_keyvalues = require("test_keyvalues")
do
local kv <close> = test_keyvalues.KeyValues.new("Config")
kv:SetName("ServerConfig")
kv:SetString("hostname", "My Server")
-- ... использовать kv ...
-- kv:close() автоматически вызывается при выходе из этой области видимости
end
-- Определенно очищается здесь!
Это рекомендуемый паттерн для Lua 5.4+, потому что:
Гарантированная очистка даже при возникновении ошибок
Финализатор сборщика мусора (__gc) вызывается автоматически при сборке мусора объекта:
Очистка сборщиком мусора
local test_keyvalues = require("test_keyvalues")
local function process_config()
local kv = test_keyvalues.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 test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- ... использовать kv ...
-- Явно очистить сейчас
kv:close()
-- Дескриптор уничтожается немедленно
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:
Обертка без деструктора
local test_keyvalues = require("test_keyvalues")
-- Класс без деструктора - просто удобная обертка
local wrapper = test_keyvalues.SomeWrapper.new()
-- Все еще имеет утилитарные методы
if wrapper:valid() then
local handle = wrapper:get()
end
-- Нет автоматической очистки - дескриптор сохраняется
-- Полезно для оберток без состояния или глобальных ресурсов
Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:
Передача владения
local test_keyvalues = require("test_keyvalues")
-- Создать родителя и ребенка
local parent = test_keyvalues.KeyValues.new("Parent")
local child = test_keyvalues.KeyValues.new("Child")
-- AddSubKey принимает владение child
parent:AddSubKey(child)
-- child теперь принадлежит parent
-- child:valid() может все еще быть true, но не используйте его!
-- parent обработает очистку
В манифесте это определяется как:
Владение в манифесте
{
"name": "AddSubKey",
"method": "Kv1AddSubKey",
"bindSelf": true,
"paramAliases": [
{
"name": "KeyValues",
"owner": true // Этот параметр принимает владение
}
]
}
Лучшая практика: После передачи владения избегайте использования объекта:
Правильная обработка владения
local test_keyvalues = require("test_keyvalues")
local parent = test_keyvalues.KeyValues.new("Parent")
local child = test_keyvalues.KeyValues.new("Child")
-- Передать владение
parent:AddSubKey(child)
-- Освободить нашу ссылку, чтобы предотвратить случайное использование
local child_handle = child:release() -- Теперь child:valid() == false
-- Использовать только через parent
local found = parent:FindKey("Child")
local test_keyvalues = require("test_keyvalues")
local parent = test_keyvalues.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 test_keyvalues = require("test_keyvalues")
local parent = test_keyvalues.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 = test_keyvalues.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> = test_keyvalues.KeyValues.new("TempConfig")
kv:SetName("CommandConfig")
-- ... использовать kv ...
-- Автоматически уничтожается при выходе из области видимости - безопасно!
end
✅ Безопасно: Локальная область видимости с ручной очисткой
Безопасный паттерн
function OnCommand(args)
local kv = test_keyvalues.KeyValues.new("TempConfig")
kv:SetName("CommandConfig")
-- ... использовать kv ...
kv:close() -- Явная очистка - безопасно!
end
✅ Безопасно: Переменная модуля с очисткой
Безопасный паттерн
local plugin_config = nil
function PluginStart()
plugin_config = test_keyvalues.KeyValues.new("PluginConfig")
plugin_config:SetName("Settings")
end
function PluginEnd()
-- Очистить переменные модуля
if plugin_config then
plugin_config:close()
plugin_config = nil
end
end
❌ Небезопасно: Глобальная без очистки
Небезопасный паттерн
-- ОПАСНО: Глобальный объект
g_config = test_keyvalues.KeyValues.new("GlobalConfig")
function PluginStart()
g_config:SetName("ServerSettings")
end
function PluginEnd()
-- ОТСУТСТВУЕТ: Нет очистки!
-- Финализатор g_config запустится после выгрузки плагина - СБОЙ!
end
❌ Небезопасно: Кэш уровня модуля без очистки
Небезопасный паттерн
-- ОПАСНО: Кэш уровня модуля
local kv_cache = {}
function CacheConfig(name)
kv_cache[name] = test_keyvalues.KeyValues.new(name)
end
function PluginEnd()
-- ОТСУТСТВУЕТ: Нет очистки кэша!
-- Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
end
Вот полный пример, показывающий, как использовать классы для системы конфигурации:
config_manager.lua
local test_keyvalues = require("test_keyvalues")
local ConfigManager = {}
ConfigManager.__index = ConfigManager
function ConfigManager.new(config_name)
local self = setmetatable({}, ConfigManager)
self.root = test_keyvalues.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 = test_keyvalues.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 при неудаче
KeyValues.new(...) вызывает ошибку, если ни один конструктор не принял аргументы. Если функция создания вернула недопустимое значение, объект создаётся, но valid() возвращает false, поэтому проверяйте оба случая:
Сбой конструктора дескриптора
local ok, kv = pcall(test_keyvalues.KeyValues.new, "Config")
if not ok then
print("Failed to create KeyValues: " .. kv)
-- Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию
elseif not kv:valid() then
print("KeyValues handle is invalid")
end
Попытка использовать объект после вызова close() или release() вызовет ошибку:
Использование закрытого дескриптора
local test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.KeyValues.new("Config")
kv:close()
-- Это вызовет ошибку
local success, err = pcall(function()
kv:SetName("Test")
end)
if not success then
print(err) -- "...: KeyValues handle is closed or not initialized"
end
-- Сначала проверьте valid(), чтобы избежать ошибок
if kv:valid() then
kv:SetName("Test")
else
print("Handle is closed!")
end
Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:
Автоматическая проверка
local test_keyvalues = require("test_keyvalues")
local kv = test_keyvalues.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 or not initialized"
end
Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:
Проблема висячей ссылки
local function get_child_ref()
local parent = test_keyvalues.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 = test_keyvalues.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 = test_keyvalues.KeyValues.new("Config")
g_cache["main"] = test_keyvalues.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
Модуль Lua встраивает Lua 5.4, поэтому переменные to-be-closed (<close>) доступны всегда. Финализатор __gc остаётся резервом для объектов, которые не закрыты и не объявлены <close>.
Lua 5.4
-- Рекомендуется: Используйте <close>
local kv <close> = test_keyvalues.KeyValues.new("Config")
kv:SetName("ServerConfig")
-- Автоматически очищается при выходе из области видимости
-- Или закройте явно
local kv2 = test_keyvalues.KeyValues.new("Config2")
kv2:close()