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

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

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

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

Зачем нужны классы?

Когда плагин экспортирует функции, создающие и уничтожающие объекты (например, Kv1Create и Kv1Destroy), вы могли бы вызывать эти функции напрямую:

// Ручной подход — подвержен ошибкам и небезопасен
let kv = test_keyvalues::Kv1Create(&Str::from("Config"));
test_keyvalues::Kv1SetName(kv, &Str::from("ServerConfig"));
let name = test_keyvalues::Kv1GetName(kv);
test_keyvalues::Kv1Destroy(kv);  // Легко забыть!

Однако у этого подхода есть несколько проблем:

  1. Утечки ресурсов: если вы забудете вызвать Kv1Destroy(), произойдёт утечка ресурса
  2. Использование после освобождения: вы можете случайно использовать хэндл после его уничтожения
  3. Отсутствие типобезопасности: сырые хэндлы (usize) не дают проверки типов на этапе компиляции
  4. Ручная очистка: вы должны самостоятельно отслеживать и уничтожать ресурсы
  5. Неидиоматичный Rust: не используется система владения Rust

Классы решают все эти проблемы, используя владение Rust и трейт Drop:

// Подход RAII — автоматически и безопасно
let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
kv.SetName(&Str::from("ServerConfig")).unwrap();
let name = kv.GetName().unwrap();
// Автоматически уничтожается, когда kv выходит из области видимости!

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

Генератор анализирует манифест вашего плагина и создаёт обёртки-структуры Rust для объектов, у которых есть и конструктор, и деструктор.

Пример сгенерированного класса

Из манифеста плагина с методами Kv1Create и Kv1Destroy генератор создаёт:

#[derive(Debug)]
pub enum KeyValuesError {
    EmptyHandle,
}

impl std::fmt::Display for KeyValuesError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            KeyValuesError::EmptyHandle => write!(f, "empty handle"),
        }
    }
}

impl std::error::Error for KeyValuesError {}

/// RAII wrapper for KeyValues handle.
#[derive(Debug)]
pub struct KeyValues {
    handle: usize,
    ownership: Ownership,
}

impl KeyValues {
    /// Creates a new KeyValues instance
    /// @param setName: The name to assign to this KeyValues instance
    #[allow(dead_code, non_snake_case)]
    pub fn new(setName: &Str) -> Result<Self, KeyValuesError> {
        let h = crate::test_keyvalues::Kv1Create(setName);
        if h == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(Self {
            handle: h,
            ownership: Ownership::Owned,
        })
    }

    /// Construct from raw handle with specified ownership
    #[allow(dead_code)]
    pub unsafe fn from_raw(handle: usize, ownership: Ownership) -> Self {
        Self { handle, ownership }
    }

    /// Returns the underlying handle
    #[allow(dead_code)]
    pub fn get(&self) -> usize {
        self.handle
    }

    /// Release ownership and return the handle. Wrapper becomes empty & borrowed.
    #[allow(dead_code)]
    pub fn release(&mut self) -> usize {
        let h = self.handle;
        self.handle = 0;
        self.ownership = Ownership::Borrowed;
        h
    }

    /// Destroys and resets the handle
    #[allow(dead_code)]
    pub fn reset(&mut self) {
        if self.handle != 0 && self.ownership == Ownership::Owned {
            crate::test_keyvalues::Kv1Destroy(self.handle);
        }
        self.handle = 0;
        self.ownership = Ownership::Borrowed;
    }

    /// Swaps two KeyValues instances
    #[allow(dead_code)]
    pub fn swap(&mut self, other: &mut KeyValues) {
        std::mem::swap(&mut self.handle, &mut other.handle);
        std::mem::swap(&mut self.ownership, &mut other.ownership);
    }

    /// Returns true if handle is valid (not empty)
    #[allow(dead_code)]
    pub fn is_valid(&self) -> bool {
        self.handle != 0
    }

    /// Gets the section name of a KeyValues instance
    /// @return The name of the KeyValues section
    #[allow(dead_code, non_snake_case)]
    pub fn GetName(&self) -> Result<Str, KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(crate::test_keyvalues::Kv1GetName(self.handle))
    }

    /// Sets the section name of a KeyValues instance
    /// @param name: The new name to assign to this KeyValues section
    #[allow(dead_code, non_snake_case)]
    pub fn SetName(&self, name: &Str) -> Result<(), KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        crate::test_keyvalues::Kv1SetName(self.handle, name);
        Ok(())
    }

    /// Finds a key by name
    /// @param keyName: The name of the key to find
    /// @return Pointer to the found KeyValues subkey, or NULL if not found
    #[allow(dead_code, non_snake_case)]
    pub fn FindKey(&self, keyName: &Str) -> Result<KeyValues, KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        Ok(unsafe { KeyValues::from_raw(crate::test_keyvalues::Kv1FindKey(self.handle, keyName), Ownership::Borrowed) })
    }

    /// Adds a subkey to this KeyValues instance
    /// @param subKey: Pointer to the KeyValues object to add as a child
    #[allow(dead_code, non_snake_case)]
    pub fn AddSubKey(&mut self, subKey: KeyValues) -> Result<(), KeyValuesError> {
        if self.handle == 0 {
            return Err(KeyValuesError::EmptyHandle);
        }
        crate::test_keyvalues::Kv1AddSubKey(self.handle, subKey.release());
        Ok(())
    }

}

impl Drop for KeyValues {
    fn drop(&mut self) {
        if self.handle != 0 && self.ownership == Ownership::Owned {
            crate::test_keyvalues::Kv1Destroy(self.handle);
        }
    }
}

impl std::cmp::PartialEq for KeyValues {
    fn eq(&self, other: &Self) -> bool {
        self.handle == other.handle
    }
}
impl std::cmp::Eq for KeyValues {}

impl std::cmp::PartialOrd for KeyValues {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        (self.handle).partial_cmp(&(other.handle))
    }
}
impl std::cmp::Ord for KeyValues {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        (self.handle).cmp(&(other.handle))
    }
}

Управление ресурсами с помощью RAII

Классы в Rust используют RAII (Resource Acquisition Is Initialization) для автоматического управления ресурсами. Это обеспечивает безопасность, гарантированную на этапе компиляции.

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

Ресурсы автоматически уничтожаются, когда объекты выходят из области видимости:

fn process_config() {
    let kv = test_keyvalues::KeyValues::new(&Str::from("ServerConfig")).unwrap();
    kv.SetName(&Str::from("Production")).unwrap();
    // Ресурс автоматически уничтожается, когда kv выходит из области видимости
} // Здесь вызывается трейт Drop — Kv1Destroy() выполняется автоматически

Владение и заимствование

Система владения Rust предотвращает распространённые ошибки:

let kv1 = test_keyvalues::KeyValues::new(&Str::from("Config1")).unwrap();
let kv2 = kv1;  // kv1 перемещён в kv2
// let name = kv1.GetName();  // ОШИБКА: kv1 был перемещён

// Правильно: заимствуйте вместо перемещения
let kv1 = test_keyvalues::KeyValues::new(&Str::from("Config1")).unwrap();
let name = kv1.GetName().unwrap();  // Заимствование
// kv1 всё ещё валиден здесь

Заимствование против перемещения

// Заимствование (доступ только для чтения)
fn read_config(kv: &KeyValues) {
    let name = kv.GetName().unwrap();
}

// Изменяемое заимствование (модификация)
fn modify_config(kv: &KeyValues) {
    kv.SetName(&Str::from("Modified")).unwrap();
}

// Передача владения (значение потребляется)
fn consume_config(kv: KeyValues) {
    // kv потребляется здесь
}

let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
read_config(&kv);       // Заимствование
modify_config(&kv);     // Изменяемое заимствование
consume_config(kv);     // Перемещение
// kv больше не валиден здесь

Семантика владения

Собственные ресурсы

Когда вы создаёте объект с помощью new(), структура владеет ресурсом:

let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
// kv владеет ресурсом (ownership = Ownership::Owned)
// Drop вызовет Kv1Destroy(), когда kv выйдет из области видимости

Заимствованные ресурсы

Когда метод возвращает указатель, которым вы не владеете (помечено "owner": false в манифесте):

let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
let child = parent.FindKey(&Str::from("ChildKey")).unwrap();
// child заимствован (ownership = Ownership::Borrowed)
// Drop у child НЕ вызовет Kv1Destroy()
// Фактическим ресурсом child владеет parent

Важно: заимствованные объекты не должны жить дольше объекта, у которого они заимствованы:

// НЕПРАВИЛЬНО — висячая ссылка
let child = {
    let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
    parent.FindKey(&Str::from("Child")).unwrap()
}; // parent уничтожается здесь вместе с child
// child теперь указывает на освобождённую память!

// Правила заимствования Rust помогают предотвратить это на этапе компиляции

Передача владения

Некоторые методы забирают владение объектами (помечено "owner": true в манифесте):

let mut parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).unwrap();
let child = test_keyvalues::KeyValues::new(&Str::from("Child")).unwrap();

parent.AddSubKey(child).unwrap();
// child перемещён в AddSubKey
// AddSubKey внутренне вызывает release(), чтобы передать владение parent
// переменная child больше не валидна

// НЕПРАВИЛЬНО: нельзя использовать child после перемещения
// child.SetName(&Str::from("NewName"));  // ОШИБКА: значение использовано после перемещения

Работа с Option

Для опциональных экземпляров классов:

fn find_config(name: &str) -> Option<KeyValues> {
    let parent = test_keyvalues::KeyValues::new(&Str::from("Parent")).ok()?;
    let child = parent.FindKey(&Str::from(name)).ok()?;

    if child.is_valid() {
        Some(child)
    } else {
        None
    }
}

match find_config("MyConfig") {
    Some(kv) => println!("Found: {}", kv.GetName().unwrap()),
    None => println!("Not found"),
}

Потокобезопасность

Система типов Rust обеспечивает потокобезопасность:

use std::sync::{Arc, Mutex};

// По умолчанию не потокобезопасно
let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();

// Сделаем потокобезопасным
let kv = Arc::new(Mutex::new(
    test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap()
));

// Клонируем Arc для потоков
let kv_clone = Arc::clone(&kv);
std::thread::spawn(move || {
    let kv = kv_clone.lock().unwrap();
    kv.SetName(&Str::from("ThreadModified")).unwrap();
});

Полный пример

Вот полный пример, демонстрирующий все концепции:

use plugify::*;

fn on_plugin_start() {
    // Создаём объект во владении
    let mut root_config = test_keyvalues::KeyValues::new(&Str::from("ServerConfig")).unwrap();
    root_config.SetName(&Str::from("Production")).unwrap();

    // Создаём подключи и передаём владение
    let database = test_keyvalues::KeyValues::new(&Str::from("Database")).unwrap();
    database.SetName(&Str::from("PostgreSQL")).unwrap();
    root_config.AddSubKey(database).unwrap();
    // database больше не валиден — владение передано

    let caching = test_keyvalues::KeyValues::new(&Str::from("Caching")).unwrap();
    caching.SetName(&Str::from("Redis")).unwrap();
    root_config.AddSubKey(caching).unwrap();

    // Find возвращает заимствованную ссылку
    let db_config = root_config.FindKey(&Str::from("Database")).unwrap();
    if db_config.is_valid() {
        println!("Database: {}", db_config.GetName().unwrap());
        // db_config заимствован — им по-прежнему владеет root_config
    }

    // root_config автоматически уничтожается по завершении функции
}

register_plugin!(
    start: on_plugin_start
);

Рекомендации

  1. Позвольте Rust управлять временем жизни: доверяйте borrow checker
    fn process() {
        let kv = test_keyvalues::KeyValues::new(&Str::from("Config")).unwrap();
        // Используем kv...
    } // Автоматически уничтожается
    
  2. Используйте ссылки для доступа только на чтение: избегайте лишних перемещений
    fn read_config(kv: &KeyValues) {
        let name = kv.GetName().unwrap();
    }
    
  3. Используйте ссылки для модификаций: методы возвращают Result
    fn modify_config(kv: &KeyValues) {
        kv.SetName(&Str::from("Modified")).unwrap();
    }
    
  4. Проверяйте валидность хэндлов: особенно у заимствованных объектов
    let child = parent.FindKey(&Str::from("Child")).unwrap();
    if child.is_valid() {
        child.SetName(&Str::from("NewName")).unwrap();
    }
    
  5. Не смешивайте модели владения: придерживайтесь системы владения Rust
    // Хорошо
    let kv = KeyValues::new(&Str::from("Config")).unwrap();
    
    // Избегайте без необходимости
    let mut kv = KeyValues::new(&Str::from("Config")).unwrap();
    let raw = kv.release();
    unsafe { KeyValues::from_raw(raw, Ownership::Owned) };
    

Преимущества системы владения Rust

Обёртки-классы Rust обеспечивают самое безопасное управление ресурсами среди всех языков Plugify:

  1. Безопасность на этапе компиляции: большинство ошибок выявляется при компиляции, а не во время выполнения
  2. Абстракции с нулевой стоимостью: гарантии безопасности не создают накладных расходов во время выполнения
  3. Отсутствие сборки мусора: детерминированная очистка без пауз GC
  4. Безопасность памяти: нет использования после освобождения, двойного освобождения и разыменования нулевых указателей
  5. Потокобезопасность: гонки данных предотвращаются на этапе компиляции
  6. Явное владение: чёткая передача ответственности

Это делает Rust самым надёжным языком для работы с классами Plugify, выявляющим ошибки до того, как они смогут вызвать проблемы в продакшене.

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

Ошибка пустого хэндла

Проблема: KeyValuesError::EmptyHandle при вызове методов.

Причина: использование заимствованного объекта с невалидным хэндлом (handle == 0).

Решение: проверяйте валидность перед использованием:

let child = parent.FindKey(&Str::from("Child")).unwrap();
if child.is_valid() {
    child.SetName(&Str::from("NewName")).unwrap();
}

Использование после перемещения

Проблема: ошибка компилятора «value used after move».

Причина: попытка использовать значение после передачи владения.

Решение: не используйте значения после их перемещения:

let child = KeyValues::new(&Str::from("Child")).unwrap();
parent.AddSubKey(child).unwrap();
// Не используйте child здесь — он был перемещён

Проблемы со временем жизни

Проблема: ошибка компилятора о временах жизни.

Причина: заимствованная ссылка живёт дольше владельца.

Решение: убедитесь, что заимствованные значения не живут дольше своих владельцев:

let db_config = {
    let parent = KeyValues::new(&Str::from("Parent")).unwrap();
    parent.FindKey(&Str::from("Database")).unwrap()  // ОШИБКА: parent уничтожается здесь
};

// Правильная версия
let parent = KeyValues::new(&Str::from("Parent")).unwrap();
let db_config = parent.FindKey(&Str::from("Database")).unwrap();
// Используйте db_config, пока parent ещё жив

Заключение

Обёртки-классы Rust используют систему владения языка, чтобы обеспечить самый безопасный и надёжный способ работы с классами Plugify. Borrow checker гарантирует безопасность памяти на этапе компиляции, предотвращая целые классы ошибок времени выполнения. Следуя принципам владения Rust и рекомендациям из этого руководства, вы сможете уверенно создавать надёжные плагины.