Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Python.
Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая автоматическое управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.
Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:
Без классов (стиль C)
from plugify.pps import example_plugin
# Создать дескриптор KeyValues вручную
kv_handle = example_plugin.Kv1Create("MyConfig")
# Установить свойства, используя дескриптор
example_plugin.Kv1SetName(kv_handle, "ServerSettings")
# Найти подключ
subkey_handle = example_plugin.Kv1FindKey(kv_handle, "Players")
# Легко забыть очистку!
example_plugin.Kv1Destroy(kv_handle)
С классами тот же код становится намного чище и безопаснее:
С классами (стиль ООП)
from plugify.pps import example_plugin
# Создать с использованием конструктора класса
with example_plugin.KeyValues("MyConfig") as kv:
# Использовать интуитивные методы
kv.SetName("ServerSettings")
# Найти подключ - возвращает экземпляр KeyValues
subkey = kv.FindKey("Players")
# Автоматическая очистка при выходе из контекста!
Преимущества:
Более чистый синтаксис - Методы вместо функций с явными дескрипторами
Автоматическое управление ресурсами - Нет необходимости вручную вызывать функции уничтожения
Безопасность типов - Лучшее автодополнение в IDE и проверка типов
Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
Питонический API - Ощущается естественным для разработчиков на Python
Когда плагин определяет классы в своем манифесте, языковой модуль Python создаёт для них классы-обертки, когда ваш плагин импортирует этот плагин. Обертки:
Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
Управляют жизненным циклом - Автоматически вызывают деструктор, когда объект больше не нужен
Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
Поддерживают менеджеры контекста - Реализуют __enter__ и __exit__ для операторов with
Предоставляют подсказки типов - plugify-gen -lang python -classes генерирует .pyi stub-файлы для поддержки IDE
Когда вы определяете классы в своем манифесте, языковой модуль Python создаёт классы-обертки с несколькими встроенными методами. Концептуально они выглядят так:
Сгенерированный класс (концептуально)
class KeyValues:
"""RAII wrapper for KeyValues handle"""
def __init__(self, *args, **kwargs):
"""Создает новый экземпляр KeyValues
Может быть вызван двумя способами:
1. Режим конструктора: KeyValues("ConfigName")
2. Режим прямого дескриптора: KeyValues(handle_value, Ownership.OWNED)
"""
from plugify.plugin import Ownership
# Сначала инициализировать в недопустимое состояние
self._handle = 0 # invalid_value
self._owned = Ownership.BORROWED
# Конструкция прямого дескриптора
if len(args) == 2 and isinstance(args[1], Ownership):
self._handle = args[0]
self._owned = args[1]
return
# Режим вызова конструктора
try:
self._handle = _plugin.Kv1Create(*args, **kwargs)
self._owned = Ownership.OWNED
except Exception as e:
raise e
def close(self):
"""Закрыть/уничтожить дескриптор, если он принадлежит"""
if not hasattr(self, '_handle'):
return
if self._handle != 0 and self._owned == Ownership.OWNED:
_plugin.Kv1Destroy(self._handle)
self._handle = 0
self._owned = Ownership.BORROWED
def __del__(self):
"""Автоматически вызывается при уничтожении объекта"""
self.close()
def __enter__(self):
"""Вход в менеджер контекста"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Выход из менеджера контекста - обеспечивает очистку"""
self.close()
return False
# Утилитарные методы
def get(self) -> int:
"""Возвращает значение базового дескриптора"""
if not hasattr(self, '_handle'):
return 0
return self._handle
def release(self) -> int:
"""Освобождает владение и возвращает дескриптор"""
if not hasattr(self, '_handle'):
return 0
tmp = self._handle
self._handle = 0
self._owned = Ownership.BORROWED
return tmp
def reset(self):
"""Сбросить дескриптор, закрыв его"""
self.close()
def valid(self) -> bool:
"""Проверяет, является ли дескриптор действительным"""
if not hasattr(self, '_handle'):
return False
return self._handle != 0
# Связанные методы (с автоматической проверкой дескриптора)
def GetName(self) -> str:
"""Получает имя секции"""
if self._handle == 0:
raise RuntimeError("KeyValues handle is closed")
return _plugin.Kv1GetName(self._handle)
def SetName(self, name: str) -> None:
"""Устанавливает имя секции"""
if self._handle == 0:
raise RuntimeError("KeyValues handle is closed")
_plugin.Kv1SetName(self._handle, name)
def FindKey(self, keyName: str) -> 'KeyValues':
"""Находит ключ по имени, возвращает принадлежащий экземпляр KeyValues"""
if self._handle == 0:
raise RuntimeError("KeyValues handle is closed")
result = _plugin.Kv1FindKey(self._handle, keyName)
# Автоматически оборачивает возвращаемое значение на основе retAlias
if result != 0:
return KeyValues(result, Ownership.OWNED)
return None
def AddSubKey(self, subKey: 'KeyValues') -> None:
"""Добавляет подключ, принимает владение параметром"""
if self._handle == 0:
raise RuntimeError("KeyValues handle is closed")
# Автоматически освобождает владение от параметра subKey
handle = subKey.release() if hasattr(subKey, 'release') else subKey
_plugin.Kv1AddSubKey(self._handle, handle)
plugify-gen -lang python -classes генерирует соответствующий .pyi stub-файл для поддержки IDE:
Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:
Использование get()
from plugify.pps import example_plugin
kv = example_plugin.KeyValues("Config")
# Получить необработанный дескриптор
raw_handle = kv.get()
print(f"Handle value: {raw_handle}")
# Передать в функцию, ожидающую необработанный дескриптор
example_plugin.Kv1GetName(kv.get())
Предупреждение: Будьте осторожны при использовании get(). Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:
Использование release()
from plugify.pps import example_plugin
def create_and_release():
kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")
# Передать владение наружу
handle = kv.release()
# kv теперь недействителен, не будет очищать
print(f"valid: {kv.valid()}") # valid: False
return handle
# Теперь мы владеем дескриптором и должны очистить его вручную
raw_handle = create_and_release()
# ... использовать raw_handle ...
example_plugin.Kv1Destroy(raw_handle) # Требуется ручная очистка!
Используйте release(), когда вам нужно:
Передать владение другой системе
Сохранить дескриптор в долгоживущей структуре данных
Взаимодействовать с кодом в стиле C, который принимает владение
Каждый класс поддерживает оператор with через __enter__ и __exit__:
Менеджер контекста
from plugify.pps import example_plugin
with example_plugin.KeyValues("Config") as kv:
kv.SetName("ServerConfig")
kv.SetString("hostname", "My Server")
# ... использовать kv ...
# Автоматически очищается при выходе из блока 'with'
Это рекомендуемый паттерн, потому что:
Гарантированная очистка даже при возникновении исключений
Деструктор (__del__) вызывается автоматически при сборке мусора объекта:
Автоматическая очистка
from plugify.pps import example_plugin
def process_config():
kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")
# ... использовать kv ...
# kv автоматически уничтожается при возврате из функции
process_config() # kv очищается здесь
Важно - Жизненный цикл плагина: Финализаторы CPython (__del__) детерминированы и вызываются, когда счетчик ссылок достигает нуля. Однако вы должны избегать ситуаций, когда финализаторы вызываются после выгрузки вашего плагина. Если вы храните объекты глобально, вы несете ответственность за их очистку в plugin_end(), иначе поведение будет неопределенным и может привести к сбоям.
Для немедленной очистки ресурсов используйте close() или удалите объект:
Ручная очистка
from plugify.pps import example_plugin
kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")
# ... использовать kv ...
# Вариант 1: Явно закрыть
kv.close()
# Вариант 2: Удалить объект
kv2 = example_plugin.KeyValues("Config2")
del kv2
# Оба приводят к немедленной очистке
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки: close() только сбрасывает дескриптор, а утилитарные методы работают как обычно.
Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:
Передача владения
from plugify.pps import example_plugin
# Создать родителя и ребенка
parent = example_plugin.KeyValues("Parent")
child = example_plugin.KeyValues("Child")
# AddSubKey принимает владение child
parent.AddSubKey(child)
# AddSubKey забрал дескриптор у child: child.valid() теперь False
# parent обработает очистку
В манифесте это определяется как:
Владение в манифесте
{
"name": "AddSubKey",
"method": "Kv1AddSubKey",
"bindSelf": true,
"paramAliases": [
{
"name": "KeyValues",
"owner": true // Этот параметр принимает владение
}
]
}
Лучшая практика: После передачи владения обращайтесь к объекту только через нового владельца:
Правильная обработка владения
from plugify.pps import example_plugin
parent = example_plugin.KeyValues("Parent")
child = example_plugin.KeyValues("Child")
# Передать владение
parent.AddSubKey(child)
print(f"valid: {child.valid()}") # valid: False, обертка отдала дескриптор
# Использовать только через parent
found = parent.FindKey("Child")
from plugify.pps import example_plugin
parent = example_plugin.KeyValues("Parent")
# MakeCopy возвращает НОВЫЙ KeyValues, которым мы владеем
copy = parent.MakeCopy()
if copy and copy.valid():
copy.SetName("ParentCopy")
# Мы ответственны за жизненный цикл copy
# Он будет автоматически очищен, когда выйдет из области видимости
Когда owner: false, метод возвращает ссылку без передачи владения:
Невладеющая ссылка
from plugify.pps import example_plugin
parent = example_plugin.KeyValues("Parent")
# GetFirstSubKey возвращает ссылку, parent все еще владеет ею
child_ref = parent.GetFirstSubKey()
if child_ref and child_ref.valid():
# Используйте ссылку, но НЕ удаляйте ее
name = child_ref.GetName()
# Не вызывайте del child_ref или release()
# child_ref будет очищен parent
Важно: С невладеющими ссылками возвращаемый объект все еще действителен (имеет дескриптор), но вы не владеете им. Удаление родителя сделает эти ссылки недействительными.
При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. CPython использует подсчет ссылок, что означает, что финализаторы (__del__) детерминированы и вызываются немедленно, когда счетчик ссылок достигает нуля. Однако, если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.
Если вы храните экземпляры классов в глобальных переменных или атрибутах уровня модуля, вы должны явно очистить их в функции plugin_end() вашего плагина:
plugin.py
from plugify.plugin import Plugin
from plugify.pps import example_plugin
# Глобальный объект - опасен, если не очищен!
g_config = None
class MyPlugin(Plugin):
def plugin_start(self):
global g_config
# Создать глобальную конфигурацию
g_config = example_plugin.KeyValues("GlobalConfig")
g_config.SetName("ServerSettings")
print("Plugin started with global config")
def plugin_end(self):
global g_config
# КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
if g_config:
g_config.close() # или del g_config
g_config = None
print("Plugin ended, global config cleaned up")
Критично: Неспособность очистить глобальные объекты в plugin_end() может привести к тому, что финализаторы запустятся после выгрузки вашего плагина, что приведет к сбоям или неопределенному поведению. Всегда явно очищайте глобальные ресурсы!
✅ Безопасно: Локальная область видимости с автоматической очисткой
Безопасный паттерн
class MyPlugin(Plugin):
def on_command(self, args):
# Локальный объект - автоматически очищается при возврате из функции
kv = example_plugin.KeyValues("TempConfig")
kv.SetName("CommandConfig")
# ... использовать kv ...
# Автоматически уничтожается здесь - безопасно!
✅ Безопасно: Менеджер контекста
Безопасный паттерн
class MyPlugin(Plugin):
def on_command(self, args):
# Менеджер контекста обеспечивает очистку перед возвратом из функции
with example_plugin.KeyValues("TempConfig") as kv:
kv.SetName("CommandConfig")
# ... использовать kv ...
# Определенно очищается здесь - безопасно!
✅ Безопасно: Переменная экземпляра с очисткой
Безопасный паттерн
class MyPlugin(Plugin):
def plugin_start(self):
self.config = example_plugin.KeyValues("PluginConfig")
self.config.SetName("Settings")
def plugin_end(self):
# Очистить переменные экземпляра
if hasattr(self, 'config') and self.config:
self.config.close()
self.config = None
❌ Небезопасно: Глобальная без очистки
Небезопасный паттерн
from plugify.pps import example_plugin
# ОПАСНО: Глобальный объект
g_config = example_plugin.KeyValues("GlobalConfig")
class MyPlugin(Plugin):
def plugin_start(self):
g_config.SetName("ServerSettings")
def plugin_end(self):
# ОТСУТСТВУЕТ: Нет очистки!
# Финализатор g_config запустится после выгрузки плагина - СБОЙ!
pass
❌ Небезопасно: Кэш уровня модуля без очистки
Небезопасный паттерн
from plugify.pps import example_plugin
# ОПАСНО: Кэш уровня модуля
_kv_cache = {}
class MyPlugin(Plugin):
def cache_config(self, name):
_kv_cache[name] = example_plugin.KeyValues(name)
def plugin_end(self):
# ОТСУТСТВУЕТ: Нет очистки кэша!
# Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
pass
Иногда вам нужно работать с необработанными дескрипторами вместе с экземплярами классов:
mixed_usage.py
from plugify.pps import example_plugin
def process_with_mixed_api():
# Создать с использованием класса
kv = example_plugin.KeyValues("Config")
# Использовать методы класса
kv.SetName("ProcessedConfig")
# Получить необработанный дескриптор для функции в стиле C
raw_handle = kv.get()
# Вызвать устаревшую функцию в стиле C
result = example_plugin.Kv1GetName(raw_handle)
# Продолжить использование методов класса
if kv.valid():
kv.SetString("status", "done")
# Автоматическая очистка все еще работает
# kv будет уничтожен при возврате из функции
def transfer_ownership_example():
# Создать и настроить
kv = example_plugin.KeyValues("Config")
kv.SetName("TransferTest")
# Освободить владение
handle = kv.release()
# kv теперь недействителен
assert not kv.valid()
# Мы должны вручную уничтожить
example_plugin.Kv1Destroy(handle)
# Или обернуть снова
kv2 = example_plugin.KeyValues("NewConfig")
# ... использовать kv2 ...
Всегда используйте менеджеры контекста - Используйте операторы with для гарантированной очистки
Используйте close() для немедленной очистки - Более явно, чем полагаться на del или сборку мусора
Очищайте глобальные объекты в plugin_end() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости, когда возможно, для автоматической очистки
Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
Уважайте владение - Не используйте объекты после передачи владения
Используйте release() экономно - Только когда вам нужен ручной контроль
Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
Доверяйте автоматической очистке для локальных объектов - Пусть __del__ обрабатывает очистку для локальных объектов, но всегда очищайте глобальные вручную
Обрабатывайте RuntimeError - Будьте готовы перехватывать исключения от методов, вызванных на закрытых дескрипторах
Проверяйте возвращаемые объекты - Методы могут возвращать None при неудаче
Если аргументы не подходят ни к одному конструктору класса, конструктор выбрасывает ValueError. Если функция создания вернула недопустимое значение, объект создаётся, но valid() возвращает False:
Сбой конструктора дескриптора
try:
kv = example_plugin.KeyValues("Config")
except ValueError as e:
print(f"Failed to create KeyValues: {e}")
else:
if not kv.valid():
print("Kv1Create returned an invalid handle")
Попытка использовать объект после вызова close(), release() или после уничтожения вызовет RuntimeError:
Использование закрытого дескриптора
from plugify.pps import example_plugin
kv = example_plugin.KeyValues("Config")
kv.close()
# Это вызовет RuntimeError
try:
kv.SetName("Test")
except RuntimeError as e:
print(f"{e}") # "KeyValues handle is closed"
# Сначала проверьте valid(), чтобы избежать исключений
if kv.valid():
kv.SetName("Test")
else:
print("Handle is closed!")
Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:
Автоматическая проверка
from plugify.pps import example_plugin
kv = example_plugin.KeyValues("Config")
handle = kv.release()
# Любой вызов метода завершится неудачей
try:
name = kv.GetName()
except RuntimeError as e:
print(f"{e}") # "KeyValues handle is closed"