Rust で COM を使って WAVE ファイルを再生してみる

[windows-rs](https://crates.io/crates/windows) と COM についてのちょっとした記事です。

# 環境

Windows10 64bit 20H2
rustc 1.58.1
windows-rs 0.32

# COM とは何か

https://docs.microsoft.com/ja-jp/windows/win32/com/component-object-model--com--portal

https://ja.wikipedia.org/wiki/Component_Object_Model

つまり Windows で使える便利なやつです。（雑）

### 余談

COM のクラスプラットフォームな実装として Mozilla が作った XPCOM があります。COM コンポーネントは言語非依存で実装できるようになっていて、XPCOM は JavaScript でも実装されたりしていました。Microsoft の COM では`IUnknown`がルートクラスですが、XPCOM では[`nsISupports`](https://searchfox.org/mozilla-central/source/xpcom/base/nsISupports.idl)がルートクラスになっています。prefix の`ns`は今はなき Netscape が由来らしいです。Netscape 6.0 を初めて使ったときは悪い意味で度肝を抜かれました。コンテキストメニューが表示されるのに 30 秒待たされるブラウザって見たことありますか？はい、私はあります。

XPCOM では参照カウント方式でリソース管理していました。しかし、循環参照によるメモリリークが問題になって、[Cycle Collector](https://searchfox.org/mozilla-central/source/xpcom/base/nsCycleCollector.cpp) が実装されることになりました。XPCOM の Cycle Collector については[この記事](https://bn.dodgson.org/bn/2006/05/07/)で簡潔にまとめられています。記事中に Graydon Hoare 氏の名前が出てきますね。Rust を作った人です。完全に余談です。

# 実装

ちょうど良い[サンプル](https://ichigopack.net/win32com/com_base_2_samplecode.html?utm_source=pocket_mylist)があったので、これを Rust に写経していきます。

## ドキュメント

### windows-rs

https://docs.rs/windows/ には何もないです。https://microsoft.github.io/windows-docs-rs/doc/windows/ を使いましょう。

### WinAPI

また、関数の引数・返り値の意味や注意事項が書いてあるので [Microsoft 公式のドキュメント](https://docs.microsoft.com/en-us/documentation/)を必ず読みましょう。

## features

[ドキュメント](https://microsoft.github.io/windows-docs-rs/doc/windows/)で使いたい関数を検索します。例えば`CoInitialize`を検索します。

https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Com/fn.CoInitialize.html

`Required features: ‘Win32_System_Com’`って書いてありますね。`CoInitialize`を使うには Win32_System_Com feature を有効にする必要があります。feature を有効にするには Cargo.toml に以下のように書きます。alloc と Win32_Foundation feature は多くの場合必須なので書いておきましょう。

```toml:Cargo.toml
[dependencies.windows]
version = "0.32"
features = [
    "alloc",
    "Win32_Foundation",
    "Win32_System_Com"
]
```

これで main.rs 内に`use windows::Win32::System::Com::CoInitialize;`と書いて関数をインポートできるようになります。

## [CoInitialize](https://docs.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitialize)

[Microsoft 公式のドキュメント](https://docs.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitialize#remarks)によると`CoInitialize`を呼んだら必ず`CoUninitialize`を呼べと書いてあります。`impl Drop`を使ってスコープを抜けたら`CoUninitialize`が呼ばれるようにしておきます。以下 C と Rust を並べて書いていきます。

```c:C
HRESULT hr;
hr = CoInitialize(NULL);
if ( FAILED(hr) ) {
    std::cout << "CoInitialize() error: " << hr << std::endl;
    return 1;
}
CoUninitialize();
```

```rust:Rust
use windows::Win32::System::Com::{CoInitialize, CoUninitialize};

struct Com;
impl Drop for Com {
    fn drop(&mut self) {
        unsafe {
            CoUninitialize();
        }
    }
}

unsafe {
    CoInitialize(ptr::null())?;
    let _com = Com;
    // ここで _com がドロップして CoUninitialize が呼ばれる
}
```

## [CoCreateInstance](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance)

`CoCreateInstance`関数は`CLSID`（クラスID）っていうのを指定してクラスに関連付けされたオブジェクトを取得します。しかし、windows-rs には今回のサンプルプログラムで必要となる`CLSID_FilterGraph`が定義されていません。なので自分で書きました。`CLSID_FilterGraph`は私の環境では`C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\shared\uuids.h`に書いてありました。値は以下の通りです。

```rust:Rust
use windows::core::GUID;

#[allow(non_upper_case_globals)]
const CLSID_FilterGraph: GUID = GUID {
    data1: 0xe436ebb3,
    data2: 0x524f,
    data3: 0x11ce,
    data4: [0x9f, 0x53, 0x00, 0x20, 0xaf, 0x0b, 0xa7, 0x70],
};
```

`CLSID_FilterGraph`が準備できたのでオブジェクトを取得しましょう。

```c:C
IMediaControl* pIFMediaControl = NULL;
hr = CoCreateInstance(
         CLSID_FilterGraph, NULL,
         CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER |
         CLSCTX_LOCAL_SERVER,
         IID_IMediaControl, (LPVOID*)&pIFMediaControl);
if ( SUCCEEDED(hr) ) {
  // omitted
}
```

```rust:Rust
use windows::Win32::{
    Media::DirectShow::IMediaControl,
    System::Com::{
        CoCreateInstance, CLSCTX_INPROC_HANDLER, CLSCTX_INPROC_SERVER, CLSCTX_LOCAL_SERVER,
    },
};

let media_ctrl: IMediaControl = CoCreateInstance(
    &CLSID_FilterGraph,
    None,
    CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER,
)?;
```

Rust は`?`オペレータでエラー処理できるし、ポインタ渡しではなくて返り値で`IMediaControl`が返ってくるのがうれしいですね。

## [QueryInterface](https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void))

windows-rs では`QueryInterface`を明示的に呼ぶ必要はありません。`windows::core::Interface`トレイトの`cast`関数を使います。

```c:C
IMediaEvent* pIFMediaEvent = NULL;
hr = pIFMediaControl->QueryInterface(
         IID_IMediaEvent, (LPVOID*)&pIFMediaEvent );
if ( SUCCEEDED(hr) ) {
  // omitted
}
```

```rust:Rust
use windows::{core::Interface, Win32::Media::DirectShow::IMediaEvent};

let media_event = media_ctrl.cast::<IMediaEvent>()?;
```

## [Release](https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release)

COM オブジェクトは使い終わったら、`Release`を呼んで参照カウントを減らす必要があります。windows-rs では`IUnknown`に`Drop`が実装されていて、自動的に`Release`されるようになっています。

```c:C
if ( pIFMediaControl != NULL ) {
    pIFMediaControl->Release();
}
if ( pIFMediaEvent != NULL ) {
    pIFMediaEvent->Release();
}
```

```rust:Rust
// 何も必要なし…
```

# まとめ

大事なことはだいたい書いたのでサンプルコードをまとめてどうぞ。

<details><summary>コピペ用サンプルコード</summary><div>

```toml:Cargo.toml
[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.32"
features = [
    "alloc",
    "Win32_Foundation",
    "Win32_System_Com",
    "Win32_Media_DirectShow",
    "Win32_System_WindowsProgramming"
]
```

```rust:main.rs
use anyhow::Result;
use std::ptr;
use windows::{
    core::{Interface, GUID},
    Win32::{
        Media::DirectShow::{IMediaControl, IMediaEvent},
        System::{
            Com::{
                CoCreateInstance, CoInitialize, CoUninitialize, CLSCTX_INPROC_HANDLER,
                CLSCTX_INPROC_SERVER, CLSCTX_LOCAL_SERVER,
            },
            WindowsProgramming::INFINITE,
        },
    },
};

/// See uuids.h
/// e436ebb3-524f-11ce-9f53-0020af0ba770 Filter Graph
#[allow(non_upper_case_globals)]
const CLSID_FilterGraph: GUID = GUID {
    data1: 0xe436ebb3,
    data2: 0x524f,
    data3: 0x11ce,
    data4: [0x9f, 0x53, 0x00, 0x20, 0xaf, 0x0b, 0xa7, 0x70],
};

struct Com;
impl Drop for Com {
    fn drop(&mut self) {
        unsafe {
            CoUninitialize();
        }
    }
}

fn main() -> Result<()> {
    unsafe {
        CoInitialize(ptr::null())?;
        let _com = Com;

        let media_ctrl: IMediaControl = CoCreateInstance(
            &CLSID_FilterGraph,
            None,
            CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER,
        )?;

        let media_event = media_ctrl.cast::<IMediaEvent>()?;
        media_ctrl.RenderFile(r"C:\Windows\Media\chimes.wav")?;
        media_ctrl.Run()?;
        media_event.WaitForCompletion(INFINITE as i32)?;
    }
    Ok(())
}
```

</div></details>

`cargo run`して「てぃろりん:musical_note:」って鳴れば大成功です。