🪟

最近のwindows-rs(0.44まで)

2023/03/02に公開

windows-rsの更新が最近落ち着いてきたので一度軽くまとめてみようかなと思った次第。

windows-rsとは

windows-rsとはRustでWin32 APIやWinRT APIを扱うためのクレートです。

https://github.com/microsoft/windows-rs

windows-rsはwindowswindows-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にもありません。

https://github.com/microsoft/win32metadata

使用方法

windows-rsの0.22以降では事前に生成されたモジュールをfeaturesに使うモジュールを書いてモジュールを使う形になっています。

0.21以前の話

0.21以前はbuild.rsに使うモジュールを書き、ビルド時にバインディングのファイルを生成していました。使いたいクレートでbuild.rsに直接書くと補完の時ビルドが走って重くなったりしたので、いちいち別にバインディング用のクレートを作ったりしていました。

例として、今回はwindowsクレートでMessageBoxW関数を使えるようにしてみます。

  1. まず、使いたい関数やインターフェイス等をwindowsクレートのドキュメントから探します。docs.rsではないので注意してください。
    https://microsoft.github.io/windows-docs-rs/doc/windows/index.html
    今回はMessageBoxWを使いたいので、MessageBoxWで検索してMessageBoxWのページまでたどり着きます。そして赤線で囲った所にあるRequired featuresを確認します。

  2. Cargo.tomldependencies.windowsテーブルを作ってfeaturesに使いたい関数やインターフェイス等が要求するモジュールを書いていきます。
    上記の1.で見たとおりMessageBoxWWin32_UI_WindowsAndMessagingWin32_Foundationを要求しているのでfeaturesにそれらを書きます。

Cargo.toml
[dependencies.windows]
version = "0.44"
features = [
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging",
]
もちろん、dependenciesテーブルに直接書くこともできます。
Cargo.toml
[dependencies]
windows = { version = "0.44", features = [
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging",
] }
  1. windows::Win32::UI::WindowsAndMessaging::MessageBoxWが使えるようになりました。
    ここで出て来るw!windowsクレートのマクロで文字列リテラルをPCWSTRにしてくれるものです。
main.rs
use windows::{core::*, Win32::UI::WindowsAndMessaging::*};

fn main() {
    unsafe {
        MessageBoxW(None, w!("hello, world!"), w!("nanka kaku"), MB_OK);
    }
}

windowsクレート

ここではwindowsクレートに関する様々なポイントについて説明します。

文字列

windowsクレートにおいて、文字列を表すものはPSTRPWSTRPCSTRPCWSTRBSTRHSTRINGがあります。

PSTRPWSTRPCSTRPCWSTRはnull終端の文字列へのポインタを持つ型です。PSTRPWSTRは可変(*mut)の文字列であり、PCSTRPCWSTRは不変(*const)の文字列を表します。PSTRPCSTRはWindowsにおけるマルチバイト文字、PWSTRPCWSTRはワイド文字を表します。

BSTRはnull終端のワイド文字の文字列に加えて先頭にサイズが付加された構造をしておりCOMの文字列として使われています。BSTRFrom<&str>From<String>などを実装しているのでRust標準の文字列をBSTRにすることができます。

HSTRINGはWinRTの文字列で参照カウントを持つ不変の文字列になります。WinRTの文字列と言っていますがWinRT以外でも使えます。HSTRINGFrom<&str>From<String>などを実装しており&HSTRINGPCWSTRにすることができるので、&strStringのなどのRust標準の文字列をHSTRINGを介してPCWSTRの引数に渡すことができます。MessageBoxWを使った場合の例を以下に示します。

fn main() {
    unsafe {
        let text = String::from("hello, world!");
        MessageBoxW(None, &HSTRING::from(text), w!("kangaerunomendou"), MB_OK);
    }
}

また、windowsクレートにはRustの文字列リテラルからHSTRINGPCSTRPCWSTRにするマクロが用意されています。それぞれh!s!w!となっており、s!はnull終端付きUTF-8の文字列、w!はnull終端付きUTF-16の文字列になります。

エラー処理

windowsクレートにはwindows::core::Resultwindows::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マクロを使ったインターフェイスの実装を順に見ていきます。

  1. implementマクロを使うにはwindowsクレートのfeaturesimplementを追加します。
    この例ではID3DIncludeも使うのでWin32_Graphics_Direct3Dも追加しています。
Cargo.toml
[dependencies.windows]
version = "0.44"
features = [
    "implement",
    "Win32_Graphics_Direct3D",
]
  1. 自分で用意した構造体に#[implement(ID3DInclude)]をつけます。
use windows::{core::*, Win32::Graphics::Direct3D::*};

#[implement(ID3DInclude)]
struct Hoge;
  1. 継承したインターフェイスの名前の後ろに_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マクロを使います。

  1. interfaceマクロを使うにはwindowsクレートのfeaturesimplementを追加します。
Cargo.toml
[dependencies.windows]
version = "0.44"
features = [
    "implement",
]
  1. PowerShellやVisual Studioに付属するuuidgen.exeなどを使ってGUIDを用意します。
    ここではf2ab3916-7567-4ea4-b508-3a387dcc1fe0とします。
  2. インターフェイスを定義します。
    GUIDを入れた#[interface()]をつけるとunsafe traitにする必要がありIUnknownを継承しないとエラーが出ます。
use windows::core::*;

#[interface("f2ab3916-7567-4ea4-b508-3a387dcc1fe0")]
unsafe trait IHoge: IUnknown {
    fn foo(&self);
}
  1. 自動的に_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::Builderon_thread_starton_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のアップデートの情報を出るたびスクラップにしているのでよければ。

https://zenn.dev/lnseab/scraps/b714d8885b2bba

Discussion