Синглтоны
В программной инженерии шаблон синглтон — это шаблон проектирования, который ограничивает создание экземпляров класса одним объектом.
Википедия: Шаблон синглтон
Почему нельзя просто использовать глобальные переменные?
Мы могли бы сделать все публичными статическими переменными, например так:
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 {
// ...
}
Это позволяет нам обеспечивать, будет ли код изменять аппаратное обеспечение или нет, на этапе компиляции, а не во время выполнения. Заметьте, что это обычно работает только в пределах одного приложения, но для систем без операционной системы наше программное обеспечение компилируется в одно приложение, так что это обычно не является ограничением.