Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

C++erが挑む実戦詰Rust: libpulse-binding の ListResult<&SinkInfo> vs ジェネリクス

2025年7月29日より、PulseAudio の Rust バインディング libpulse-binding を使って音声デバイスを管理するツール autopulsed を開発しています。これはその開発中に起きた実際の問題と、その解決までの道のりです。

問題:autopulsed での重複コード

sink 系と source 系で同じようなコードが重複していたので、ジェネリクスで統一しようと考えました。

実際のコード

fn make_sink_callback(&self) -> impl FnMut(ListResult<&SinkInfo>) + 'static {
    let weak_origin = Rc::downgrade(&self.origin);
    let mut should_update = false;
    move |list_result| {
        // Sink用の処理
    }
}

fn make_source_callback(&self) -> impl FnMut(ListResult<&SourceInfo>) + 'static {
    let weak_origin = Rc::downgrade(&self.origin);
    let mut should_update = false;
    move |list_result| {
        // Source用の処理(Sinkとほぼ同じ)
    }
}

素朴な associated type → 型エラー

まず素朴にジェネリクスを使ってみました。型パラメータとして Sink または Source を渡すことで、コールバック内部でそれぞれに固有の処理を呼べるようにしようと考えたのです。

trait DeviceType {
    type Info;
}

struct Sink;
impl DeviceType for Sink {
    type Info = SinkInfo;
}

struct Source;
impl DeviceType for Source {
    type Info = SourceInfo;
}

ですがこれはコンパイルできません。

error[E0106]: missing lifetime specifier

SinkInfoSinkInfo<'a> という形でライフタイムパラメータが必要だからです。

ライフタイム付き associated type

そこで associated type にライフタイムパラメータを追加してみます。

trait DeviceType {
    type Info<'a>;
}

struct Sink;
impl DeviceType for Sink {
    type Info<'a> = SinkInfo<'a>;
}

fn make_device_callback<T: DeviceType>(&self) -> impl FnMut(ListResult<&T::Info>) + 'static {
    let weak_origin = Rc::downgrade(&self.origin);
    let mut should_update = false;
    move |list_result| {
        // 共通処理
    }
}

今度は make_device_callback の定義でエラーになります。

error[E0107]: missing generics for associated type `DeviceType::Info`

associated type では &SinkInfo のような省略形は使えないようです。そこで次のように修正しますが…

trait DeviceType {
    type Info<'a>;
}

struct Sink;
impl DeviceType for Sink {
    type Info<'a> = SinkInfo<'a>;
}

fn make_device_callback<T: DeviceType>(&self) -> impl FnMut(ListResult<&T::Info<'_>>) + 'static {
    let weak_origin = Rc::downgrade(&self.origin);
    let mut should_update = false;
    move |list_result| {
        // 共通処理
    }
}

fn make_sink_callback(&self) -> impl FnMut(ListResult<&SinkInfo>) + 'static {
    self.make_device_callback::<Sink>()  // ここでエラー!
}

エラーメッセージはこうです。

error: implementation of `FnMut` is not general enough
   |
   = note: `impl FnMut(ListResult<&<Sink as DeviceType>::Info<'_>>)` must implement `FnMut<(ListResult<&'0 SinkInfo<'1>>,)>`, for any two lifetimes `'0` and `'1`...
   = note: ...but it actually implements `FnMut<(ListResult<&SinkInfo<'_>>,)>`, for some specific lifetime `'_`

同じように書いているはずのジェネリクスを使わない元のコードは問題なく動作します。なぜでしょうか?

ヒント:ListResult<&SinkInfo> の真名

&SinkInfoという一見シンプルな型には、実は2つの独立したライフタイムが隠されていました。

// 見た目
ListResult<&SinkInfo>

// 完全形態(省略されているライフタイム)
ListResult<&'a SinkInfo<'b>>
  • 'a: 参照自体のライフタイム
  • 'b: SinkInfo 内部のデータ(Cow<'b, str>など)のライフタイム

libpulse-binding の SinkInfo の定義を見ると次のようになっています。

pub struct SinkInfo<'a> {
    pub name: Option<Cow<'a, str>>,
    pub description: Option<Cow<'a, str>>,
    // ...
}

実験:単一ライフタイム for<'a> → 解決しない

単一のライフタイムで解決しようとしても失敗します:

fn make_device_callback<T: DeviceType>() 
    -> impl for<'a> FnMut(ListResult<&'a T::Info<'a>>) + 'static 
{
    move |result| {
        // ...
    }
}

// 使おうとすると...
get_sink_info_list(make_device_callback::<Sink>());  // エラー!

エラーはこうです。

error: implementation of `FnMut` is not general enough
   |
   = note: `impl for<'a> FnMut(ListResult<&'a <Sink as DeviceType>::Info<'a>>)` must implement `FnMut<(ListResult<&'0 SinkInfo<'1>>,)>`, for any two lifetimes `'0` and `'1`...
   = note: ...but it actually implements `FnMut<(ListResult<&'2 SinkInfo<'2>>,)>`, for some specific lifetime `'2`

for<'a> では、参照のライフタイムとSinkInfo内部のライフタイムが同じ('2)になってしまいます。

正解:for<'a, 'b> による完全詠唱

次のように、2つの独立したライフタイムを明示的に宣言する必要がありました!

fn make_device_callback<T: DeviceType>(&self) 
    -> impl for<'a, 'b> FnMut(ListResult<&'a T::Info<'b>>) + 'static 
{
    let weak_origin = Rc::downgrade(&self.origin);
    let mut should_update = false;
    move |list_result| {
        match list_result {
            ListResult::Item(info) => {
                // infoの型: &'a T::Info<'b>
                // 処理...
            },
            ListResult::End => { /* ... */ },
            ListResult::Error => { /* ... */ }
        }
    }
}

ただし、info を扱うときにライフタイムも一貫していないといけないので注意が必要です。

trait DeviceType {
    type Info<'a>;

    // Sink/SourceInfo の構造から name を取り出そうとした場合、
    // ライフタイムが入力参照のライフタイム'aに制限される
    fn get_name<'a, 'b>(info: &'a Self::Info<'b>) -> Option<&'a str>;
    //                                                       ^^
    // &'b str ではなく &'a str
}

これで完全に動作します!(実際のコード

別解:マクロ

この複雑さを避けてマクロを使うのも賢明な選択です。

macro_rules! make_device_callback {
    ($self:expr, $device_type:ty) => {{
        let weak_origin = Rc::downgrade(&$self.origin);
        let mut should_update = false;
        move |list_result| {
            // マクロ展開時に具体的な型になるので
            // ライフタイムの問題を回避できる
        }
    }};
}

fn make_sink_callback(&self) -> impl FnMut(ListResult<&SinkInfo>) + 'static {
    make_device_callback!(self, Sink)
}

C++だとマクロはアンチパターンなのでつい避けたくなりますが、Rust ならそんなに心配しなくて良いのかもしれません。

得られたもの

一見何の変哲もない &Hoge に、実際には2つのライフタイムが関わっている場合があることを学びました。なかなか雰囲気からは想像しにくい事実でしたが、無事解決できてよかったです。このような厳格な仕組みが Rust の安全性を支えているんですね。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。