Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Go.
Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе структур с идиоматичной обработкой ошибок Go.
Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:
Без классов (стиль C)
package main
import "test_keyvalues"
func processConfig() {
// Создать дескриптор KeyValues вручную
kvHandle := test_keyvalues.Kv1Create("MyConfig")
// Установить свойства, используя дескриптор
test_keyvalues.Kv1SetName(kvHandle, "ServerSettings")
// Найти подключ
subkeyHandle := test_keyvalues.Kv1FindKey(kvHandle, "Players")
// Легко забыть очистку!
test_keyvalues.Kv1Destroy(subkeyHandle)
test_keyvalues.Kv1Destroy(kvHandle)
}
С классами тот же код становится намного чище и безопаснее:
С классами (стиль ООП)
package main
import "test_keyvalues"
func processConfig() {
// Создать с использованием конструктора
kv := test_keyvalues.NewKeyValuesKv1Create("MyConfig")
defer kv.Close() // Автоматическая очистка при возврате из функции
// Использовать интуитивные методы с обработкой ошибок
if err := kv.SetName("ServerSettings"); err != nil {
panic(err)
}
// Найти подключ - возвращает экземпляр KeyValues
subkey, err := kv.FindKey("Players")
if err != nil {
panic(err)
}
// Автоматическая очистка через defer!
}
Преимущества:
Более чистый синтаксис - Методы вместо функций с явными дескрипторами
Идиоматичная обработка ошибок Go - Методы возвращают error вместо использования исключений
Очистка на основе defer - Используйте defer для детерминированной очистки ресурсов на основе области видимости
Безопасность типов - Лучшее автодополнение в IDE и проверки во время компиляции
Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
Предотвращение копирования - Защита noCopy гарантирует, что дескрипторы не будут случайно скопированы
Отслеживание владения - Явная семантика владения предотвращает ошибки двойного освобождения
Важный паттерн:
Используйте Defer для очистки: Go не имеет деструкторов, как C++, но defer обеспечивает детерминированную очистку в области видимости функции. Всегда используйте defer obj.Close() после создания принадлежащих объектов.
Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:
Использование Get()
package main
import (
"fmt"
"test_keyvalues"
)
func getRawHandle() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
defer kv.Close()
// Получить необработанный дескриптор
rawHandle := kv.Get()
fmt.Printf("Handle value: %d\n", rawHandle)
// Передать в функцию, ожидающую необработанный дескриптор
someCFunction(kv.Get())
}
Предупреждение: Будьте осторожны при использовании Get(). Возвращаемый дескриптор все еще принадлежит экземпляру структуры и будет уничтожен при очистке экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова Release() экземпляр структуры становится недействительным и не будет вызывать деструктор:
Использование Release()
package main
import (
"fmt"
"test_keyvalues"
)
func createAndRelease() uintptr {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
kv.SetName("ServerConfig")
// Передать владение наружу
handle := kv.Release()
// kv теперь недействителен, не будет очищать
fmt.Println(kv.IsValid()) // false
// Defer не нужен - мы освободили владение
return handle
}
func useReleased() {
// Теперь мы владеем дескриптором и должны очистить его вручную
rawHandle := createAndRelease()
// ... использовать rawHandle ...
test_keyvalues.Kv1Destroy(rawHandle) // Требуется ручная очистка!
}
Используйте Release(), когда вам нужно:
Передать владение другой системе
Сохранить дескриптор в долгоживущей структуре данных
Взаимодействовать с кодом в стиле C, который принимает владение
Оператор defer в Go обеспечивает детерминированную очистку в области видимости функции:
Паттерн Defer (рекомендуется)
package main
import "test_keyvalues"
func processConfig() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
defer kv.Close() // Гарантированная очистка при возврате из функции
kv.SetName("ServerConfig")
kv.SetString("hostname", "My Server")
// ... использовать kv ...
// Автоматически очищается здесь через defer
}
Это рекомендуемый паттерн, потому что:
Гарантированная очистка даже при панике
Четкая область жизни ресурса
Детерминированное время очистки
Идиоматично для Go
Лучшая практика: Всегда используйте defer obj.Close() сразу после создания принадлежащего объекта. Это обеспечивает очистку даже если функция паникует или возвращается досрочно.
Plugify использует runtime.Cleanup для регистрации финализаторов, которые очищают ресурсы во время сборки мусора. Однако это недетерминировано и на это не следует полагаться:
Резервная финализация
package main
import "test_keyvalues"
func processConfig() {
kv := test_keyvalues.NewKeyValuesKv1Create("Config")
kv.SetName("ServerConfig")
// ... использовать kv ...
// НЕТ defer - полагается на финализацию (ПЛОХО!)
}
// kv в конечном итоге будет очищен финализатором
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!
Важно - Жизненный цикл плагина: Финализаторы вызываются сборщиком мусора Go при сборке объектов. Однако вы должны избегать ситуаций, когда финализаторы вызываются после выгрузки вашего плагина. Если вы храните объекты глобально, вы несете ответственность за их очистку в PluginEnd(), иначе поведение будет неопределенным и может привести к сбоям.
Примечание: Время работы финализатора недетерминировано и зависит от циклов GC. Всегда используйте defer для предсказуемой очистки.
Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:
Обертка без деструктора
package main
import "test_keyvalues"
func useWrapper() {
// Структура без деструктора - просто удобная обертка
wrapper := test_keyvalues.NewSomeWrapper()
// Все еще имеет утилитарные методы
if wrapper.IsValid() {
handle := wrapper.Get()
// ... использовать handle ...
}
// Нет автоматической очистки - дескриптор сохраняется
// Полезно для оберток без состояния или глобальных ресурсов
}
package main
import "test_keyvalues"
func returnOwnership() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
// FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
child, err := parent.FindKey("Settings")
if err != nil {
panic(err)
}
defer child.Close() // Мы владеем им, должны очистить
if child.IsValid() {
child.SetName("UpdatedSettings")
// Мы ответственны за жизненный цикл child
}
}
Когда owner: false, метод возвращает ссылку без передачи владения:
Невладеющая ссылка
package main
import "test_keyvalues"
func nonOwningRef() {
parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
defer parent.Close()
// GetFirstSubKey возвращает ссылку, parent все еще владеет ею
childRef, err := parent.GetFirstSubKey()
if err != nil {
panic(err)
}
if childRef.IsValid() {
// Используйте ссылку, но НЕ закрывайте или освобождайте ее
name, _ := childRef.GetName()
_ = name
// childRef будет очищен parent
}
}
Важно: С невладеющими ссылками возвращаемый объект все еще действителен (имеет дескриптор), но вы не владеете им. Закрытие родителя сделает эти ссылки недействительными.
При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. Если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.
Если вы храните экземпляры классов в глобальных переменных или переменных уровня пакета, вы должны явно очистить их в функции PluginEnd() вашего плагина:
plugin.go
package main
import (
"github.com/untrustedmodders/go-plugify"
"test_keyvalues"
)
// Глобальный объект - опасен, если не очищен!
var gConfig *test_keyvalues.KeyValues
func PluginStart() {
// Создать глобальную конфигурацию
gConfig = test_keyvalues.NewKeyValuesKv1Create("GlobalConfig")
gConfig.SetName("ServerSettings")
plugify.Log("Plugin started with global config")
}
func PluginEnd() {
// КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
if gConfig != nil {
gConfig.Close()
gConfig = nil
}
plugify.Log("Plugin ended, global config cleaned up")
}
Критично: Неспособность очистить глобальные объекты в PluginEnd() может привести к тому, что финализаторы запустятся после выгрузки вашего плагина, что приведет к сбоям или неопределенному поведению. Всегда явно очищайте глобальные ресурсы!
func onCommand(args []string) {
// Локальный объект с defer - автоматически очищается
kv := test_keyvalues.NewKeyValuesKv1Create("TempConfig")
defer kv.Close()
kv.SetName("CommandConfig")
// ... использовать kv ...
// Автоматически уничтожается при возврате из функции - безопасно!
}
Всегда используйте defer - Вызывайте defer obj.Close() сразу после создания принадлежащих объектов
Проверяйте ошибки - Все методы возвращают ошибки, всегда проверяйте их
Очищайте глобальные в PluginEnd() - Критично: Явно очищайте все глобальные или уровня пакета экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
Не defer освобожденные объекты - Если метод принимает владение, не defer очистку на переданном объекте
Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости с defer, когда возможно
Проверяйте IsValid() - Особенно после операций, которые могут завершиться неудачей
Уважайте владение - Не используйте объекты после передачи владения (они автоматически освобождаются)
Не копируйте - Защита noCopy обнаружит копии с помощью go vet
Используйте возврат ошибок - Не паникуйте в библиотечном коде, возвращайте ошибки
Обрабатывайте nil возвраты - Методы могут возвращать nil при неудаче
Запускайте go vet - Обнаруживает случайные копии и другие проблемы