最近のwindows-rs(0.44まで)
windows-rsの更新が最近落ち着いてきたので一度軽くまとめてみようかなと思った次第。
windows-rs
とは
windows-rsとはRustでWin32 APIやWinRT APIを扱うためのクレートです。
windows-rsはwindows
とwindows-sys
の2つのクレートからなり、それぞれの特徴は以下のようになります。
-
windows
- できるだけRustの慣習に合わせた形になったAPI
- COMやWinRT APIをサポート
- コンパイル時間が長い
-
windows-sys
- CスタイルのAPIをほぼそのままバインド
-
no_std
をサポート - COMやWinRT APIをサポートしない
- コンパイル時間が短い
ちなみに、Win32 APIの関数やメソッドはunsafe
になっていますが、WinRT APIのメソッドは安全に利用できます。
また、Xaml APIやMsHtml API、廃止予定や使用できないAPIは除外されています。除外されたAPIについて詳しくはGetting StartedのWhat APIs are includedを参照してください。
この記事はほとんどwindows
クレートに関するものになります。windows-sys
クレートはほぼ生のバインディングなので書くことがなくて。
win32metadataとの関係
windows-rsはwin32metadataを用いてコードを生成しています。windows-rsの更新ごとにwindows-rsが参照しているwin32metadataの更新が行われるため、最新のAPIが入っていないことがよくあります。また現在、win32metadaにはマクロやヘッダーオンリーの関数は入っていないためwindows-rsにもありません。
使用方法
windows-rsの0.22以降では事前に生成されたモジュールをfeatures
に使うモジュールを書いてモジュールを使う形になっています。
0.21以前の話
0.21以前はbuild.rsに使うモジュールを書き、ビルド時にバインディングのファイルを生成していました。使いたいクレートでbuild.rsに直接書くと補完の時ビルドが走って重くなったりしたので、いちいち別にバインディング用のクレートを作ったりしていました。
例として、今回はwindows
クレートでMessageBoxW
関数を使えるようにしてみます。
-
まず、使いたい関数やインターフェイス等を
windows
クレートのドキュメントから探します。docs.rsではないので注意してください。
https://microsoft.github.io/windows-docs-rs/doc/windows/index.html
今回はMessageBoxW
を使いたいので、MessageBoxW
で検索してMessageBoxW
のページまでたどり着きます。そして赤線で囲った所にあるRequired features
を確認します。
-
Cargo.toml
にdependencies.windows
テーブルを作ってfeatures
に使いたい関数やインターフェイス等が要求するモジュールを書いていきます。
上記の1.で見たとおりMessageBoxW
はWin32_UI_WindowsAndMessaging
とWin32_Foundation
を要求しているのでfeatures
にそれらを書きます。
[dependencies.windows]
version = "0.44"
features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
]
もちろん、dependenciesテーブルに直接書くこともできます。
[dependencies]
windows = { version = "0.44", features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
] }
-
windows::Win32::UI::WindowsAndMessaging::MessageBoxW
が使えるようになりました。
ここで出て来るw!
はwindows
クレートのマクロで文字列リテラルをPCWSTR
にしてくれるものです。
use windows::{core::*, Win32::UI::WindowsAndMessaging::*};
fn main() {
unsafe {
MessageBoxW(None, w!("hello, world!"), w!("nanka kaku"), MB_OK);
}
}
windows
クレート
ここではwindows
クレートに関する様々なポイントについて説明します。
文字列
windows
クレートにおいて、文字列を表すものはPSTR
、PWSTR
、PCSTR
、PCWSTR
、BSTR
、HSTRING
があります。
PSTR
とPWSTR
、PCSTR
とPCWSTR
はnull終端の文字列へのポインタを持つ型です。PSTR
とPWSTR
は可変(*mut
)の文字列であり、PCSTR
とPCWSTR
は不変(*const
)の文字列を表します。PSTR
とPCSTR
はWindowsにおけるマルチバイト文字、PWSTR
とPCWSTR
はワイド文字を表します。
BSTR
はnull終端のワイド文字の文字列に加えて先頭にサイズが付加された構造をしておりCOMの文字列として使われています。BSTR
はFrom<&str>
やFrom<String>
などを実装しているのでRust標準の文字列をBSTR
にすることができます。
HSTRING
はWinRTの文字列で参照カウントを持つ不変の文字列になります。WinRTの文字列と言っていますがWinRT以外でも使えます。HSTRING
はFrom<&str>
やFrom<String>
などを実装しており&HSTRING
をPCWSTR
にすることができるので、&str
やString
のなどのRust標準の文字列をHSTRING
を介してPCWSTR
の引数に渡すことができます。MessageBoxW
を使った場合の例を以下に示します。
fn main() {
unsafe {
let text = String::from("hello, world!");
MessageBoxW(None, &HSTRING::from(text), w!("kangaerunomendou"), MB_OK);
}
}
また、windows
クレートにはRustの文字列リテラルからHSTRING
、PCSTR
、PCWSTR
にするマクロが用意されています。それぞれh!
、s!
、w!
となっており、s!
はnull終端付きUTF-8の文字列、w!
はnull終端付きUTF-16の文字列になります。
エラー処理
windows
クレートにはwindows::core::Result
やwindows::core::Error
が定義されています。windows::core::Error
はRust標準の::std::error::Error
が実装されていますし、windows::core::Result
はエラーの型をwindows::core::Error
にしたRust標準の::std::result::Result
のエイリアスなので問題なく使えるでしょう。
windows
クレートに定義されたAPIについて、HRESULT
を返すような関数やメソッドはHRESULT
の代わりにwindows::core::Result
で返すようになっています。一方で、エラー内容をGetLastError
で取得するようなWin32 APIの関数はwindows::core::Result
ではないことが多々あるので、GetLastError
の代わりにwindows::core::Error::from_win32
を使いましょう。
無名構造体/無名共用体
Win32 APIの構造体の中に無名構造体や無名共用体が含まれている場合は、フィールドの名前はAnonymous
で、Anonymous
の型はそれが含まれている構造体の名前の後ろに_0
がついたものになります。
D3D12_RESOURCE_BARRIER
を例にすると、無名共用体の型はD3D12_RESOURCE_BARRIER_0
となり、フィールドの名前はAnonymous
になります。
#[repr(C)]
pub struct D3D12_RESOURCE_BARRIER {
pub Type: D3D12_RESOURCE_BARRIER_TYPE,
pub Flags: D3D12_RESOURCE_BARRIER_FLAGS,
pub Anonymous: D3D12_RESOURCE_BARRIER_0,
}
ちなみに複数の無名構造体や無名共用体がある場合は、フィールドの名前はAnonymous1
からの連番になり、型の接尾辞は_0
からの連番になります。
引数がOptionやスライスに
win32metadataには引数がポインタの時にヌルポインタを渡せるかどうかの情報があり、windows-rsはこれを見てヌルポインタを渡せる場合に引数の型をOption
で包んでいます。また、ポインタが配列どうかや配列の要素数はどの引数かの情報もあり、windows-rsはこれらの情報から配列のポインタの引数と配列の要素数の引数を合わせてスライスの引数にします。
ただ、この辺の仕様はまだ安定しておらず度々更新が入るので今後のアップデートを見ていく必要があると思います。
COM
ここではwindows
クレートにおけるCOMの扱いについて説明します。
windows
クレートではCOMのインターフェイスは全てスマートポインタの形で提供されます。clone
でコピーすれば自動的にAddRef
されますし、Drop
で自動的にRelease
されます。
また、全てのインターフェイスはwindows::core::Interface
トレイトを実装しており、
このトレイトには
- 自身のIIDを示す
IID
関連定数 -
QueryInterface
の代わりになるcast
メソッド - 弱参照を生成する
downgrade
メソッド
が定義されています。
windows::core::Interface
トレイトのメンバは使いたい場合は、
use windows::core::Interface;
を書きましょう。
implement
マクロ
インターフェイスを実装するためのDirect3DのID3DInclude
やTSFのITextStoreACP
のようにユーザが実装して使う事を想定しているインターフェイスがあります。C++ではインターフェイスを継承して実装しますが、windows
クレートではimplement
マクロとインターフェイスと一緒に定義されているトレイトを実装することで実現しています。
ここではID3DInclude
を例にimplement
マクロを使ったインターフェイスの実装を順に見ていきます。
-
implement
マクロを使うにはwindows
クレートのfeatures
にimplement
を追加します。
この例ではID3DInclude
も使うのでWin32_Graphics_Direct3D
も追加しています。
[dependencies.windows]
version = "0.44"
features = [
"implement",
"Win32_Graphics_Direct3D",
]
- 自分で用意した構造体に
#[implement(ID3DInclude)]
をつけます。
use windows::{core::*, Win32::Graphics::Direct3D::*};
#[implement(ID3DInclude)]
struct Hoge;
- 継承したインターフェイスの名前の後ろに
_Impl
を付けたトレイトを実装します。
ID3DInclude
ならID3DInclude_Impl
になります。
impl ID3DInclude_Impl for Hoge {
fn Open(
&self,
includetype: D3D_INCLUDE_TYPE,
pfilename: &PCSTR,
pparentdata: *const std::ffi::c_void,
ppdata: *mut *mut std::ffi::c_void,
pbytes: *mut u32
) -> Result<()> {
Ok(())
}
fn Close(&self, pdata: *const std::ffi::c_void) -> Result<()> {
Ok(())
}
}
以上のようにしてインターフェイスの実装ができます。
ちなみに、#[implement()]
のカッコ内のインターフェイスは複数書くこともできます。そして、書いたインターフェイス分だけ実装しましょう。
interface
マクロ
インターフェイスを定義するためのCOMインターフェイスを定義するにはinterface
マクロを使います。
-
interface
マクロを使うにはwindows
クレートのfeatures
にimplement
を追加します。
[dependencies.windows]
version = "0.44"
features = [
"implement",
]
- PowerShellやVisual Studioに付属するuuidgen.exeなどを使ってGUIDを用意します。
ここではf2ab3916-7567-4ea4-b508-3a387dcc1fe0
とします。 - インターフェイスを定義します。
GUIDを入れた#[interface()]
をつけるとunsafe trait
にする必要がありIUnknown
を継承しないとエラーが出ます。
use windows::core::*;
#[interface("f2ab3916-7567-4ea4-b508-3a387dcc1fe0")]
unsafe trait IHoge: IUnknown {
fn foo(&self);
}
- 自動的に
_Impl
がついたトレイトが定義されるので前述のimplement
マクロを使って実装できます。
#[implement(Hoge)]
struct Hoge;
impl IHoge_Impl for Hoge {
unsafe fn foo(&self) {
}
}
IUnknown
を継承しないインターフェイス
IUnknown
を継承しないインターフェイスを定義することも可能です。定義するにはGUIDを書かずに#[interface]
をつけるだけになります。
#[interface]
unsafe trait IHoge {
fn foo(&self);
}
IUnknown
を継承しないインターフェイスを実装するときにはimplement
マクロを使えませんが、そのまま自動的に定義された_Impl
のトレイトを実装することができます。
struct Hoge;
impl IHoge_Impl for Hoge {
fn foo(&self) {
}
}
IUnknown
を継承しないインターフェイスにはnew
メソッドが定義され、これを使うとスコープ内で有効なオブジェクトを作ることができます。
let object = Hoge;
let hoge = IHoge::new(&object);
ただIUnknown
を継承しないインターフェイスはCOMとして行儀のよくないものなので、理由がなければ定義してまで使うものではないと思われます。
tokioのランタイムをマルチスレッドでCOMを使えるようにする
COMを使えるようにするには、スレッドを作るたびに作ったスレッドでCoInitializeEx
を呼び、CoInitailizeEx
を呼んだスレッドを終了する直前までにCoUninitialize
を呼ぶ必要があります。
tokioのランタイムでも以下の例のようにtokio::runtime::Builder
のon_thread_start
とon_thread_stop
を使ってワーカースレッドごとにCOMを使えるようにできます。on_thread_start
でランタイムがスレッドを作った直後にCoInitializeEx
が呼ばれ、on_thread_stop
でランタイムがスレッドを終了する直前にCoUninitialize
が呼ばれるようになります。
use windows::Win32::System::Com::*;
let rt = tokio::runtime::Builder::new_multi_thread()
.on_thread_start(|| unsafe {
CoInitializeEx(None, COINIT_MULTITHREADED).unwrap();
})
.on_thread_stop(|| unsafe {
CoUninitialize();
})
.build()
.unwrap();
WinRT
ここではwindows
クレートにおけるWinRTの扱いについて説明したいのですが
WinRTのオブジェクトは全てIInspectable
を実装しており、 WinRTのメソッドにはunsafe
がついていないので安全に使えます。
IAsyncOperation
IAsyncOperation
インターフェイスは::std::future::Future
を実装しているのでRustの非同期の仕組みを使うことができます。
終わりに
windows-rsにはまだ足りてない部分もありますが、かなりいい感じに仕上がってきていると思います。
さらに、Windows Driver Kitのメタデータであるwdkmetadataのサポートもmasterにマージされたので、もうすぐ使えるようになるかもしれません。
WindowsでRustを使うときの一助になれば幸いです。
あと、windows-rsのアップデートの情報を出るたびスクラップにしているのでよければ。
Discussion