Rust lifetimes patterns или где-то рядом
Здравствуй добрый человек. Я не так давно начал изучать Rust, пытаюсь написать пет-проект. В процессе наткнулся на страшные ругательства со стороны компилятора, и если честно не очень понимаю, что же ему не нравится. Буду благодарен любым объяснениям и исправлениям.
Предметная область:
Если кратко, пишу небольшой safe wrap над Windows Media Foundation... но это к делу не относится.
Чуть детальнее - есть любой компьютер с (возможно несколькими) микрофонами. Задача - вывести список микрофонов. В целях конспирации (а также чуть-чуть из-за иностранного влияния) Компьютер - это Host
, а микрофон - Device
, в коде использую это соглашение.
Ближе к делу. Есть два трейта, которые в точности описывают, что я хочу:
pub trait AudioDevice {
fn readable_name(&self) -> crate::error::Result<&str>;
}
pub trait Host {
type Device: AudioDevice;
fn audio_devices(&self) -> crate::error::Result<&[Self::Device]>;
}
Почему методы возвращают crate::error::Result<..>
? Потому что Media Foundation. В целом это можно игнорировать. Важнее, что я на хосте хочу метод, возвращающий слайс девайсов.
Что хочу получить:
Чтобы вот это компилировалось:
use voice::traits::{AudioDevice, Host};
fn main() -> voice::Result<()> {
let host = voice::platform::Host::new()?;
for device in host.audio_devices()?.iter() {
println!("{:?}", device.readable_name());
}
Ok(())
}
Но компилятор беспощаден:
error: lifetime may not live long enough
При чем здесь лайфтаймы? Давайте по порядку..
Детали
Итак, я начал с описания RAII для "состояния инициализации Media Foundation".. короче. Надо вызвать MFStartup
и MFShutdown
в начале и в конце программы соответственно. Отслеживанием этого занимается структура Context
, которая в Context::new
и Drop
делает все эти вещи потокобезопасно, также на Context
определил некоторые чуть более безопасные обертки над чистыми сишными вызовами. В целом это дела не касается, поэтому код Context
не привожу.
Теперь реализация Device
для Media Foundation.
pub struct AudioDevice<'ctx> {
source: &'ctx IMFActivate,
readable_name: OnceCell<String>,
}
impl<'ctx> AudioDevice<'ctx> {
pub fn new(source: &'ctx IMFActivate) -> Self {
Self {
source,
readable_name: OnceCell::new(),
}
}
fn get_readable_name(&self) -> crate::error::Result<String> {
let mut name: PWSTR = PWSTR::null();
let mut name_len: u32 = 0;
unsafe {
self.source.GetAllocatedString(
&MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
&mut name,
&mut name_len,
)?;
Ok(name.to_string()?)
}
}
}
impl<'ctx> crate::traits::AudioDevice for AudioDevice<'ctx> {
fn readable_name(&self) -> crate::error::Result<&str> {
let readable_name = self
.readable_name
.get_or_try_init(|| self.get_readable_name())?;
Ok(readable_name.as_str())
}
}
impl<'ctx> From<&'ctx IMFActivate> for AudioDevice<'ctx> {
fn from(source: &'ctx IMFActivate) -> Self {
AudioDevice::new(source)
}
}
Действительно много буков. на что стоит обратить внимание: AudioDevice
содержит ссылку на объект Media Foundation, из которого можно получить читаемое имя (как? смотри в методе AudioDevice::get_readable_name
). Этот объект живет не дольше, чем Context
, из которого он был получен. А также из соображений производительности readable_name
инициализируется лениво. Вот собственно и вся сложность.
Все ближе к коллапсу - Devices
:
struct Devices<'ctx> {
capture: OnceCell<&'ctx [IMFActivate]>,
devices: OnceCell<SmallVec<[AudioDevice<'ctx>; 10]>>,
}
impl<'ctx> Devices<'ctx> {
fn new() -> Self {
Self {
capture: OnceCell::new(),
devices: OnceCell::new(),
}
}
fn get(&self, context: &'ctx Context) -> crate::error::Result<&[AudioDevice]> {
let devices = self
.devices
.get_or_try_init(|| -> crate::error::Result<_> {
let capture_devices = self
.capture
.get_or_try_init(|| self.get_capture_devices(context))?;
Ok(SmallVec::from_iter(
capture_devices
.iter()
.map(|source| AudioDevice::from(source)),
))
})?;
Ok(devices.as_ref())
}
fn get_capture_devices(
&self,
context: &'ctx Context,
) -> crate::error::Result<&'ctx [IMFActivate]> {
let attributes = context.create_attributes(1)?;
unsafe {
// SAFETY: Media Foundation MUST be initialized.
attributes.SetGUID(
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_GUID,
)?;
// SAFETY: Attributes set to capture audio devices.
Ok(context.enum_device_sources(&attributes)?)
}
}
}
Уже страшно. В чем идея: Media Foundation API возвращает (указатель + размер) сишный массив объектов source (именно тех, которые используются в Device
для получения читабельного имени). Отсюда мысль: собрать из того, что возвращает API, слайс и засунуть его в структуру для дальнейшего использования. это поле capture
. Но в итоге ведь хотим получить слайс AudioDevice
'ов, поэтому существует второе поле - devices
- фактически просто вектор девайсов. Ну и да, опять же, все инициализируется лениво.
ВАЖНО: Хочется, чтобы Host::audio_devices
возвращал ссылку на поле devices
.
На этом этапе запускал небольшой тест, все прекрасно работает:
#[cfg(test)]
mod tests {
use crate::traits::AudioDevice;
use super::*;
#[test]
fn list_devices() -> crate::error::Result<()> {
let context = Context::new()?;
let devices = Devices::new();
for device in devices.get(&context)?.iter() {
println!("{:?}", device.readable_name())
}
Ok(())
}
}
# Output example
Ok("Микрофон (Steam Streaming Microphone)")
Ok("Микрофон (Realtek High Definition Audio)")
И теперь маленький взрыв:
pub struct Host<'ctx> {
context: Context,
devices: Devices<'ctx>,
}
impl<'ctx> Host<'ctx> {
pub fn new() -> crate::error::Result<Self> {
Ok(Self {
context: Context::new()?,
devices: Devices::new(),
})
}
}
impl<'ctx> crate::traits::Host for Host<'ctx> {
type Device = AudioDevice<'ctx>;
fn audio_devices(&self) -> crate::error::Result<&[Self::Device]> {
Ok(self.devices.get(&self.context)?)
}
}
error: lifetime may not live long enough
--> voice\src\platform\media_foundation\mod.rs:28:12
|
24 | impl<'ctx> crate::traits::Host for Host<'ctx> {
| ---- lifetime `'ctx` defined here
...
27 | fn audio_devices(&self) -> crate::error::Result<&[Self::Device]> {
| - let's call the lifetime of this reference `'1`
28 | Ok(self.devices.get(&self.context)?)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'ctx`
Собственно сам вопрос:
Почему оно не компилируется и как правильно размазать лайфтаймы по функции Host::audio_devices
? Если честно, пробовал всякое. был момент, когда компилятор ругался, что host
в main
живет недостаточно долго. Но я так и не понял, зачем self
по мнению компилятора должен пережить context
.
Ответы (1 шт):
self
должен пережить ctx
поскольку если экземпляр Host удалить, то ссылка на context
станет невалидной.
В теории можно сделать так
fn audio_devices(&'ctx self) -> Result<&'ctx [AudioDevice<'ctx>]> {
Ok(self.devices.get(&self.context)?)
}
Но в trait
это описать у меня не получилось. Если хочешь двигаться в этом направлении, то можно поискать на тему self-referential structs
.
Я бы принимал Context
как аргумент в trait
, либо завернул IMFActivate
в Rc
и клонировал бы его в нужных местах. Возможно нужно раздавать Weak
, вместо полноценных Rс
.