LoginSignup
13
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-13

windows-rs と COM についてのちょっとした記事です。

環境

Windows10 64bit 20H2
rustc 1.58.1
windows-rs 0.32

COM とは何か

つまり Windows で使える便利なやつです。(雑)

余談

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

XPCOM では参照カウント方式でリソース管理していました。しかし、循環参照によるメモリリークが問題になって、Cycle Collector が実装されることになりました。XPCOM の Cycle Collector についてはこの記事で簡潔にまとめられています。記事中に Graydon Hoare 氏の名前が出てきますね。Rust を作った人です。完全に余談です。

実装

ちょうど良いサンプルがあったので、これを Rust に写経していきます。

ドキュメント

windows-rs

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

WinAPI

また、関数の引数・返り値の意味や注意事項が書いてあるので Microsoft 公式のドキュメントを必ず読みましょう。

features

ドキュメントで使いたい関数を検索します。例えばCoInitializeを検索します。

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

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

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

CoInitialize

Microsoft 公式のドキュメントによるとCoInitializeを呼んだら必ずCoUninitializeを呼べと書いてあります。impl Dropを使ってスコープを抜けたらCoUninitializeが呼ばれるようにしておきます。以下 C と Rust を並べて書いていきます。

C
HRESULT hr;
hr = CoInitialize(NULL);
if ( FAILED(hr) ) {
    std::cout << "CoInitialize() error: " << hr << std::endl;
    return 1;
}
CoUninitialize();
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

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
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
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
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

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

C
IMediaEvent* pIFMediaEvent = NULL;
hr = pIFMediaControl->QueryInterface(
         IID_IMediaEvent, (LPVOID*)&pIFMediaEvent );
if ( SUCCEEDED(hr) ) {
  // omitted
}
Rust
use windows::{core::Interface, Win32::Media::DirectShow::IMediaEvent};

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

Release

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

C
if ( pIFMediaControl != NULL ) {
    pIFMediaControl->Release();
}
if ( pIFMediaEvent != NULL ) {
    pIFMediaEvent->Release();
}
Rust
// 何も必要なし…

まとめ

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

コピペ用サンプルコード
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"
]
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(())
}

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

13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7