Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Python.

Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая автоматическое управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
from plugify.pps import s2sdk

# Создать дескриптор KeyValues вручную
kv_handle = s2sdk.Kv1Create("MyConfig")

# Установить свойства, используя дескриптор
s2sdk.Kv1SetName(kv_handle, "ServerSettings")

# Найти подключ
subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players")

# Легко забыть очистку!
s2sdk.Kv1Destroy(subkey_handle)
s2sdk.Kv1Destroy(kv_handle)

С классами тот же код становится намного чище и безопаснее:

С классами (стиль ООП)
from plugify.pps import s2sdk

# Создать с использованием конструктора класса
with s2sdk.KeyValues("MyConfig") as kv:
    # Использовать интуитивные методы
    kv.SetName("ServerSettings")

    # Найти подключ - возвращает экземпляр KeyValues
    subkey = kv.FindKey("Players")

    # Автоматическая очистка при выходе из контекста!

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Автоматическое управление ресурсами - Нет необходимости вручную вызывать функции уничтожения
  • Безопасность типов - Лучшее автодополнение в IDE и проверка типов
  • Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
  • Питонический API - Ощущается естественным для разработчиков на Python

Как работают классы

Когда плагин определяет классы в своем манифесте, Plugify автоматически генерирует обертки классов Python, которые:

  1. Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
  2. Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
  3. Управляют жизненным циклом - Автоматически вызывают деструктор, когда объект больше не нужен
  4. Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
  5. Поддерживают менеджеры контекста - Реализуют __enter__ и __exit__ для операторов with
  6. Предоставляют подсказки типов - Генерируют .pyi stub-файлы для поддержки IDE

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    {
      "name": "Kv1Create",
      "funcName": "Kv1Create",
      "paramTypes": [
        { "name": "setName", "type": "string" }
      ],
      "retType": { "type": "ptr64" }
    },
    {
      "name": "Kv1Destroy",
      "funcName": "Kv1Destroy",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "void" }
    },
    {
      "name": "Kv1GetName",
      "funcName": "Kv1GetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "string" }
    },
    {
      "name": "Kv1SetName",
      "funcName": "Kv1SetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" },
        { "name": "name", "type": "string" }
      ],
      "retType": { "type": "void" }
    }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        {
          "name": "GetName",
          "method": "Kv1GetName",
          "bindSelf": true
        },
        {
          "name": "SetName",
          "method": "Kv1SetName",
          "bindSelf": true
        }
      ]
    }
  ]
}

Объяснение ключевых полей:

  • name: Имя класса Python (рекомендуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально)
  • bindings: Массив методов, доступных в классе
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код Python

Когда вы определяете классы в своем манифесте, Plugify генерирует обертки классов 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 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)

Соответствующий .pyi stub-файл также генерируется для поддержки IDE:

plugin.pyi
from typing import Optional
from plugify import Ownership

class KeyValues:
    """RAII wrapper for KeyValues handle"""

    def __init__(self, setName: str) -> None: ...
    def __enter__(self) -> 'KeyValues': ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> bool: ...

    def close(self) -> None: ...
    def get(self) -> int: ...
    def release(self) -> int: ...
    def reset(self) -> None: ...
    def valid(self) -> bool: ...

    def GetName(self) -> str: ...
    def SetName(self, name: str) -> None: ...
    def FindKey(self, keyName: str) -> Optional['KeyValues']: ...
    def AddSubKey(self, subKey: 'KeyValues') -> None: ...

Встроенные утилитарные методы

Каждый сгенерированный класс включает несколько утилитарных методов для управления дескриптором:

valid() - Проверка действительности дескриптора

Возвращает True, если дескриптор действителен (не равен invalidValue):

Использование valid()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")

if kv.valid():
    kv.SetName("ServerConfig")
    print("Handle is valid")
else:
    print("Handle is invalid")

# После release дескриптор становится недействительным
handle = kv.release()
print(kv.valid())  # False

get() - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование get()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")

# Получить необработанный дескриптор
raw_handle = kv.get()
print(f"Handle value: {raw_handle}")

# Передать в функцию, ожидающую необработанный дескриптор
some_c_function(kv.get())

release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:

Использование release()
from plugify.pps import s2sdk

def create_and_release():
    kv = s2sdk.KeyValues("Config")
    kv.SetName("ServerConfig")

    # Передать владение наружу
    handle = kv.release()

    # kv теперь недействителен, не будет очищать
    print(kv.valid())  # False

    return handle

# Теперь мы владеем дескриптором и должны очистить его вручную
raw_handle = create_and_release()
# ... использовать raw_handle ...
s2sdk.Kv1Destroy(raw_handle)  # Требуется ручная очистка!

Используйте release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

close() - Ручная очистка

Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:

Использование close()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")

# Явно закрыть дескриптор сейчас
kv.close()

# Дескриптор теперь недействителен
print(kv.valid())  # False

# Методы вызовут RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(f"Error: {e}")  # "KeyValues handle is closed"

Метод close():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Вызывается автоматически - С помощью __del__ и __exit__
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором

reset() - Псевдоним для Close

Удобный метод, который вызывает close():

Использование reset()
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")

# Reset эквивалентен close
kv.reset()

print(kv.valid())  # False

Управление ресурсами

Классы Python, сгенерированные Plugify, поддерживают несколько паттернов очистки:

Паттерн менеджера контекста (рекомендуется)

Когда определен деструктор, классы автоматически поддерживают оператор with через __enter__ и __exit__:

Менеджер контекста
from plugify.pps import s2sdk

with s2sdk.KeyValues("Config") as kv:
    kv.SetName("ServerConfig")
    kv.SetString("hostname", "My Server")
    # ... использовать kv ...
# Автоматически очищается при выходе из блока 'with'

Это рекомендуемый паттерн, потому что:

  • Гарантированная очистка даже при возникновении исключений
  • Четкая область жизни ресурса
  • Питонический и знакомый разработчикам на Python

Автоматическая очистка

Деструктор (__del__) вызывается автоматически при сборке мусора объекта:

Автоматическая очистка
from plugify.pps import s2sdk

def process_config():
    kv = s2sdk.KeyValues("Config")
    kv.SetName("ServerConfig")
    # ... использовать kv ...
    # kv автоматически уничтожается при возврате из функции

process_config()  # kv очищается здесь

Ручная очистка

Для немедленной очистки ресурсов используйте close() или удалите объект:

Ручная очистка
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# ... использовать kv ...

# Вариант 1: Явно закрыть
kv.close()

# Вариант 2: Удалить объект
kv2 = s2sdk.KeyValues("Config2")
del kv2

# Оба приводят к немедленной очистке

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:

Обертка без деструктора
from plugify.pps import s2sdk

# Класс без деструктора - просто удобная обертка
wrapper = s2sdk.SomeWrapper()

# Все еще имеет утилитарные методы
if wrapper.valid():
    handle = wrapper.get()

# Нет автоматической очистки - дескриптор сохраняется
# Полезно для оберток без состояния или глобальных ресурсов

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:

Передача владения
from plugify.pps import s2sdk

# Создать родителя и ребенка
parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")

# AddSubKey принимает владение child
parent.AddSubKey(child)

# child теперь принадлежит parent
# child.valid() может все еще быть True, но не используйте его!
# parent обработает очистку

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "subKey",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения избегайте использования объекта:

Правильная обработка владения
from plugify.pps import s2sdk

parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")

# Передать владение
parent.AddSubKey(child)

# Освободить нашу ссылку, чтобы предотвратить случайное использование
child_handle = child.release()  # Теперь child.valid() == False

# Использовать только через parent
found = parent.FindKey("Child")

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
from plugify.pps import s2sdk

parent = s2sdk.KeyValues("Parent")

# FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
child = parent.FindKey("Settings")

if child and child.valid():
    child.SetName("UpdatedSettings")
    # Мы ответственны за жизненный цикл child
    # Он будет автоматически очищен, когда выйдет из области видимости

В манифесте:

Возврат владения в манифесте
{
  "name": "FindKey",
  "method": "Kv1FindKey",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
from plugify.pps import s2sdk

parent = s2sdk.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__) детерминированы и вызываются немедленно, когда счетчик ссылок достигает нуля. Однако, если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.

Управление глобальными объектами

Если вы храните экземпляры классов в глобальных переменных или атрибутах уровня модуля, вы должны явно очистить их в функции pluginEnd() вашего плагина:

plugin.py
from plugify import PluginEntryPoint
from plugify.pps import s2sdk

# Глобальный объект - опасен, если не очищен!
g_config = None

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        global g_config
        # Создать глобальную конфигурацию
        g_config = s2sdk.KeyValues("GlobalConfig")
        g_config.SetName("ServerSettings")
        print("Plugin started with global config")

    def pluginEnd(self):
        global g_config
        # КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
        if g_config:
            g_config.close()  # или del g_config
            g_config = None
        print("Plugin ended, global config cleaned up")

Безопасные паттерны

✅ Безопасно: Локальная область видимости с автоматической очисткой

Безопасный паттерн
class MyPlugin(PluginEntryPoint):
    def onCommand(self, args):
        # Локальный объект - автоматически очищается при возврате из функции
        kv = s2sdk.KeyValues("TempConfig")
        kv.SetName("CommandConfig")
        # ... использовать kv ...
        # Автоматически уничтожается здесь - безопасно!

✅ Безопасно: Менеджер контекста

Безопасный паттерн
class MyPlugin(PluginEntryPoint):
    def onCommand(self, args):
        # Менеджер контекста обеспечивает очистку перед возвратом из функции
        with s2sdk.KeyValues("TempConfig") as kv:
            kv.SetName("CommandConfig")
            # ... использовать kv ...
        # Определенно очищается здесь - безопасно!

✅ Безопасно: Переменная экземпляра с очисткой

Безопасный паттерн
class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        self.config = s2sdk.KeyValues("PluginConfig")
        self.config.SetName("Settings")

    def pluginEnd(self):
        # Очистить переменные экземпляра
        if hasattr(self, 'config') and self.config:
            self.config.close()
            self.config = None

❌ Небезопасно: Глобальная без очистки

Небезопасный паттерн
from plugify.pps import s2sdk

# ОПАСНО: Глобальный объект
g_config = s2sdk.KeyValues("GlobalConfig")

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        g_config.SetName("ServerSettings")

    def pluginEnd(self):
        # ОТСУТСТВУЕТ: Нет очистки!
        # Финализатор g_config запустится после выгрузки плагина - СБОЙ!
        pass

❌ Небезопасно: Кэш уровня модуля без очистки

Небезопасный паттерн
from plugify.pps import s2sdk

# ОПАСНО: Кэш уровня модуля
_kv_cache = {}

class MyPlugin(PluginEntryPoint):
    def cacheConfig(self, name):
        _kv_cache[name] = s2sdk.KeyValues(name)

    def pluginEnd(self):
        # ОТСУТСТВУЕТ: Нет очистки кэша!
        # Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
        pass

Контрольный список очистки

Перед выгрузкой вашего плагина (pluginEnd()), убедитесь:

  1. ✅ Все глобальные экземпляры классов явно закрыты или удалены
  2. ✅ Все коллекции уровня модуля (списки, словари, множества), содержащие экземпляры классов, очищены
  3. ✅ Все переменные экземпляра класса явно очищены
  4. ✅ Не осталось ссылок на экземпляры классов в долгоживущих структурах данных
  5. ✅ Циклические ссылки разорваны (если есть)

Полный пример: Система конфигурации

Вот полный пример, показывающий, как использовать классы для системы конфигурации:

config_manager.py
from plugify.pps import s2sdk
from typing import Optional

class ConfigManager:
    def __init__(self, config_name: str):
        """Инициализировать менеджер конфигурации"""
        self.root = s2sdk.KeyValues(config_name)

        if not self.root.valid():
            raise RuntimeError(f"Failed to create config: {config_name}")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Очистка обрабатывается self.root
        return False

    def create_section(self, section_name: str) -> bool:
        """Создать новую секцию конфигурации"""
        section = s2sdk.KeyValues(section_name)

        if not section.valid():
            return False

        self.root.AddSubKey(section)
        return True

    def get_section(self, section_name: str) -> Optional[s2sdk.KeyValues]:
        """Получить секцию конфигурации"""
        section = self.root.FindKey(section_name)

        if section and section.valid():
            return section
        return None

    def set_value(self, section_name: str, key: str, value: str) -> bool:
        """Установить значение конфигурации"""
        section = self.get_section(section_name)

        if section:
            section.SetString(key, value)
            return True
        return False

    def get_value(self, section_name: str, key: str, default: str = "") -> str:
        """Получить значение конфигурации"""
        section = self.get_section(section_name)

        if section:
            return section.GetString(key, default)
        return default

    def save(self, filename: str) -> bool:
        """Сохранить конфигурацию в файл"""
        if not self.root.valid():
            return False
        return self.root.SaveToFile(filename)

    def load(self, filename: str) -> bool:
        """Загрузить конфигурацию из файла"""
        if not self.root.valid():
            return False
        return self.root.LoadFromFile(filename)

# Использование с менеджером контекста
with ConfigManager("ServerConfig") as config:
    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")

# Ресурсы автоматически очищаются

Расширенный пример: Ручное управление дескриптором

Иногда вам нужно работать с необработанными дескрипторами вместе с экземплярами классов:

mixed_usage.py
from plugify.pps import s2sdk

def process_with_mixed_api():
    # Создать с использованием класса
    kv = s2sdk.KeyValues("Config")

    # Использовать методы класса
    kv.SetName("ProcessedConfig")

    # Получить необработанный дескриптор для функции в стиле C
    raw_handle = kv.get()

    # Вызвать устаревшую функцию в стиле C
    result = s2sdk.SomeLegacyFunction(raw_handle)

    # Продолжить использование методов класса
    if kv.valid():
        kv.SaveToFile("output.kv")

    # Автоматическая очистка все еще работает
    # kv будет уничтожен при возврате из функции

def transfer_ownership_example():
    # Создать и настроить
    kv = s2sdk.KeyValues("Config")
    kv.SetName("TransferTest")

    # Освободить владение
    handle = kv.release()

    # kv теперь недействителен
    assert not kv.valid()

    # Мы должны вручную уничтожить
    s2sdk.Kv1Destroy(handle)

    # Или обернуть снова
    kv2 = s2sdk.KeyValues("NewConfig")
    # ... использовать kv2 ...

Лучшие практики

  1. Всегда используйте менеджеры контекста - Используйте операторы with для гарантированной очистки
  2. Используйте close() для немедленной очистки - Более явно, чем полагаться на del или сборку мусора
  3. Очищайте глобальные объекты в pluginEnd() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
  4. Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости, когда возможно, для автоматической очистки
  5. Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
  6. Уважайте владение - Не используйте объекты после передачи владения
  7. Используйте release() экономно - Только когда вам нужен ручной контроль
  8. Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
  9. Доверяйте автоматической очистке для локальных объектов - Пусть __del__ обрабатывает очистку для локальных объектов, но всегда очищайте глобальные вручную
  10. Обрабатывайте RuntimeError - Будьте готовы перехватывать исключения от методов, вызванных на закрытых дескрипторах
  11. Проверяйте возвращаемые объекты - Методы могут возвращать None при неудаче

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

Ошибка "Failed to create X"

Если конструктор вызывает исключение, базовая функция создания вернула недопустимый дескриптор:

Сбой конструктора дескриптора
try:
    kv = s2sdk.KeyValues("Config")
except RuntimeError as e:
    print(f"Failed to create KeyValues: {e}")
    # Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию

Использование закрытых или освобожденных объектов

Попытка использовать объект после вызова close(), release() или после уничтожения вызовет RuntimeError:

Использование закрытого дескриптора
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
kv.close()

# Это вызовет RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(e)  # "KeyValues handle is closed"

# Сначала проверьте valid(), чтобы избежать исключений
if kv.valid():
    kv.SetName("Test")
else:
    print("Handle is closed!")

Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:

Автоматическая проверка
from plugify.pps import s2sdk

kv = s2sdk.KeyValues("Config")
handle = kv.release()

# Любой вызов метода завершится неудачей
try:
    name = kv.GetName()
except RuntimeError as e:
    print(e)  # "KeyValues handle is closed"

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:

Проблема висячей ссылки
def get_child_ref():
    parent = s2sdk.KeyValues("Parent")
    child = parent.GetFirstSubKey()  # Невладеющая ссылка
    return child  # ПЛОХО: parent будет уничтожен!

# Это опасно!
child_ref = get_child_ref()
# child_ref теперь указывает на уничтоженную память

# Лучший подход:
def get_child_owned():
    parent = s2sdk.KeyValues("Parent")
    child = parent.FindKey("Child")  # Возвращает принадлежащий экземпляр
    # Нужно держать parent живым или передать владение child
    return parent.FindKey("Child")  # Безопасно, если FindKey возвращает принадлежащий

Утечки памяти

Если вы испытываете утечки памяти:

  1. Проверьте циклические ссылки - GC Python обрабатывает большинство случаев, но циклические ссылки могут задержать очистку
  2. Используйте менеджеры контекста - Гарантированная очистка с операторами with
  3. Проверьте использование release() - Убедитесь, что освобожденные дескрипторы в конечном итоге уничтожаются
  4. Проверьте владение - Убедитесь, что вы не держите ссылки на принадлежащие объекты

Сбои при выгрузке плагина

Если ваш плагин падает при выгрузке:

  1. Проверьте глобальные объекты - Убедитесь, что все глобальные экземпляры классов очищены в pluginEnd()
  2. Проверьте коллекции уровня модуля - Очистите все списки, словари или множества, содержащие экземпляры классов
  3. Проверьте переменные экземпляра - Очистите переменные экземпляра класса в pluginEnd()
  4. Добавьте явную очистку - Используйте close() или del для всех долгоживущих объектов перед выгрузкой плагина
Исправление сбоя при выгрузке
# Глобальные объекты, которые вызывали сбои
g_config = None
g_cache = {}

class MyPlugin(PluginEntryPoint):
    def pluginStart(self):
        global g_config, g_cache
        g_config = s2sdk.KeyValues("Config")
        g_cache["main"] = s2sdk.KeyValues("Main")

    def pluginEnd(self):
        global g_config, g_cache

        # Очистить глобальные объекты
        if g_config:
            g_config.close()
            g_config = None

        # Очистить кэшированные объекты
        for kv in g_cache.values():
            kv.close()
        g_cache.clear()

        print("All resources cleaned up safely")

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина