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 шт):

Автор решения: vessd

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, вместо полноценных .

→ Ссылка