Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Синглтоны

В программной инженерии шаблон синглтон — это шаблон проектирования, который ограничивает создание экземпляров класса одним объектом.

Википедия: Шаблон синглтон

Почему нельзя просто использовать глобальные переменные?

Мы могли бы сделать все публичными статическими переменными, например так:

static mut THE_SERIAL_PORT: SerialPort = SerialPort;

fn main() {
    let _ = unsafe {
        THE_SERIAL_PORT.read_speed();
    };
}

Но у этого подхода есть несколько проблем. Это изменяемая глобальная переменная, и в Rust взаимодействие с такими переменными всегда небезопасно. Кроме того, эти переменные видны во всей программе, что означает, что проверяющий заимствования не может помочь вам отслеживать ссылки и владение этими переменными.

Как это сделать в Rust?

Вместо того чтобы делать наше периферийное устройство глобальной переменной, мы можем создать структуру, в данном случае названную PERIPHERALS, которая содержит Option<T> для каждого из наших периферийных устройств.

struct Peripherals {
    serial: Option<SerialPort>,
}
impl Peripherals {
    fn take_serial(&mut self) -> SerialPort {
        let p = replace(&mut self.serial, None);
        p.unwrap()
    }
}
static mut PERIPHERALS: Peripherals = Peripherals {
    serial: Some(SerialPort),
};

Эта структура позволяет нам получить единственный экземпляр нашего периферийного устройства. Если мы попытаемся вызвать take_serial() более одного раза, наш код вызовет панику!

fn main() {
    let serial_1 = unsafe { PERIPHERALS.take_serial() };
    // Это вызовет панику!
    // let serial_2 = unsafe { PERIPHERALS.take_serial() };
}

Хотя взаимодействие с этой структурой является unsafe, после того как мы получили содержащийся в ней SerialPort, нам больше не нужно использовать unsafe или саму структуру PERIPHERALS.

Это имеет небольшую накладную стоимость во время выполнения, поскольку нам нужно обернуть структуру SerialPort в Option, и нам придется один раз вызвать take_serial(), однако эта небольшая начальная стоимость позволяет нам использовать проверяющий заимствования...

#[entry]
fn main(cx: main::Context) -> ! {
    // Получение доступа к периферийным устройствам ядра
    let core: CorePeripherals = cx.core;
        
    // Устройство-специфичные периферийные устройства
    let device: lm3s6965::Peripherals = cx.device;
}

Но зачем?

Но как эти синглтоны существенно влияют на работу нашего кода на Rust?

impl SerialPort {
    const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

    fn read_speed(
        &self // <------ Это действительно очень важно
    ) -> u32 {
        unsafe {
            ptr::read_volatile(Self::SER_PORT_SPEED_REG)
        }
    }
}

Здесь действуют два важных фактора:

  • Поскольку мы используем синглтон, есть только один способ или место для получения структуры SerialPort.
  • Чтобы вызвать метод read_speed(), мы должны иметь владение или ссылку на структуру SerialPort.

Эти два фактора вместе означают, что доступ к аппаратному обеспечению возможен только в том случае, если мы соответствующим образом удовлетворили проверяющий заимствования, что означает, что у нас никогда не будет нескольких изменяемых ссылок на одно и то же аппаратное обеспечение!

fn main() {
    // Отсутствует ссылка на `self`! Не сработает.
    // SerialPort::read_speed();

    let serial_1 = unsafe { PERIPHERALS.take_serial() };

    // Вы можете читать только то, к чему у вас есть доступ
    let _ = serial_1.read_speed();
}

Относитесь к вашему оборудованию как к данным

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

Это может изменять настройки оборудования:

fn setup_spi_port(
    spi: &mut SpiPort,
    cs_pin: &mut GpioPin
) -> Result<()> {
    // ...
}

А это — нет:

fn read_button(gpio: &GpioPin) -> bool {
    // ...
}

Это позволяет нам обеспечивать, будет ли код изменять аппаратное обеспечение или нет, на этапе компиляции, а не во время выполнения. Заметьте, что это обычно работает только в пределах одного приложения, но для систем без операционной системы наше программное обеспечение компилируется в одно приложение, так что это обычно не является ограничением.