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 は多くの場合必須なので書いておきましょう。
[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 を並べて書いていきます。
HRESULT hr;
hr = CoInitialize(NULL);
if ( FAILED(hr) ) {
std::cout << "CoInitialize() error: " << hr << std::endl;
return 1;
}
CoUninitialize();
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
に書いてありました。値は以下の通りです。
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
が準備できたのでオブジェクトを取得しましょう。
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
}
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
関数を使います。
IMediaEvent* pIFMediaEvent = NULL;
hr = pIFMediaControl->QueryInterface(
IID_IMediaEvent, (LPVOID*)&pIFMediaEvent );
if ( SUCCEEDED(hr) ) {
// omitted
}
use windows::{core::Interface, Win32::Media::DirectShow::IMediaEvent};
let media_event = media_ctrl.cast::<IMediaEvent>()?;
Release
COM オブジェクトは使い終わったら、Release
を呼んで参照カウントを減らす必要があります。windows-rs ではIUnknown
にDrop
が実装されていて、自動的にRelease
されるようになっています。
if ( pIFMediaControl != NULL ) {
pIFMediaControl->Release();
}
if ( pIFMediaEvent != NULL ) {
pIFMediaEvent->Release();
}
// 何も必要なし…
まとめ
大事なことはだいたい書いたのでサンプルコードをまとめてどうぞ。
コピペ用サンプルコード
[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"
]
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
して「てぃろりん」って鳴れば大成功です。