何かと話題の Rust についていつかやろうと思ってずっと後回しにしてきましたが、最近 Windows の公式の Rust Crate が中々よさそうだということを知ったので、Rust で色々な Windows API を呼び出すプログラムを作成したりデバッグしたりしてみることにしました。
参考:microsoft/windows-rs: Rust for Windows
以下は C-style Windows API を呼び出すことのできる Crate です。
参考:windows-sys - crates.io: Rust Package Registry
また、こちらは COM などの API を呼び出すことができる Crate のようです。
参考:windows - crates.io: Rust Package Registry
このあたりの Crate を使用した実装を色々試していきたいと思います。
もくじ
Rust に関するメモ書き
プログラムの実装を行う前に、そもそも Rust がどういう言語なのかについて調べたことを簡単にまとめておくことにします。
Rust は一般に以下のようなメリットがある言語と言われているようです。
- 実行速度が速い
- 安全性が担保されている
- エディションにより互換性が担保されている
- 機能、ツールが豊富
特に、実行速度についてはガベージコレクションを行わず、機械語に直接コンパイルされることもあり、C/C++ に次ぐ性能を発揮できるそうです。
また、C++ のようにゼロコスト抽象化を目指している点も Rust が高いパフォーマンスを発揮する一因であるらしいです。
参考:The Rust Programming Language - The Rust Programming Language
Rust のインストールと開発環境構築
今回は Rust で Windows API を呼び出すプログラムを実装したいので、Linux ではなく Windows で開発環境の構築を行います。
基本的には以下の公式ドキュメントの手順に従います。
参考:Windows で Rust 用の開発環境を設定する | Microsoft Learn
Rust の環境構築のため、まずはビルドツールのインストールのために Visual Studio をインストールします。
その際、[.Net desktop development] と [Desktop development with C++] を同時にインストールしておきます。
続いて、以下からダウンロードしたインストーラで Rust を Windows にインストールします。
参考:Install Rust - Rust Programming Language
これで最低限の Rust の環境構築は完了ですが、VSCode を IDE として使用するために rust-analyzer と CodeLLDB の拡張機能をインストールします。
参考:Visual Studio Code をインストールする | Microsoft Learn
Rust のプロジェクトを作成する
一通りのセットアップが完了したら、以下のコマンドを実行することで HelloWorld プロジェクトを作成することができます。
cargo new HelloWorld
作成したプロジェクトには、main.rs と Cargo.toml ファイル、そして .gitignore ファイルが含まれています。
Cargo.toml ファイルは TOML 形式で記述されたマニフェストファイルです。
cargo new で作成されたプロジェクトには、デフォルトで以下のマニフェストが記述されています。
[package]
name = "HelloWorld"
version = "0.1.0"
edition = "2024"
[dependencies]
参考:The Manifest Format - The Cargo Book
[package]
セクションは、Cargo で必須のセクションであり、パッケージの名前とバージョンが記述されています。
また、edition にはパッケージがどの edition でビルドされるかを指定する値が入ります。
Rust では、この edition という仕組みによって後方互換性を維持している(らしい)です。
参考:Rust 2024に向けてRust 2021を理解しよう~TechFeed Conference 2022講演より | gihyo.jp
作成したプロジェクトは、Cargo.toml と同じ階層で以下のコマンドを実行することでビルドできます。
cargo build
デフォルトでは target\debug\
直下に EXE ファイルが生成されました。
特に何も設定していなくても、同じフォルダに pdb ファイルが生成されるのでデバッグは容易そうです。
ビルドオプションを変更する
ちなみに、cargo ではオプションを指定しないとデフォルトで dev プロファイルが選択されるらしく、これが dev フォルダ配下にビルドしたファイルが作成される理由のようです。
例えば、以下のようなオプションを使用すると release フォルダ内にファイルがビルドされます。
cargo build --release
参考:リリースプロファイルでビルドをカスタマイズする - The Rust Programming Language 日本語版
この時、各プロファイルでのビルド時のオプションは Cargo.toml ファイルで記述できます。
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
debug = false
使用可能なオプションについては以下がわかりやすかったです。
参考:cargo buildのコンパイルオプションについて #Rust - Qiita
ビルド対象を指定する
プロジェクトを作成した直後のデフォルトの設定の場合、cargo build を実行すると src/main.rs がビルドされます。
しかし、今回のように単一ファイルで実装したプログラムを簡単にテストしたい場合には src フォルダ直下に bin フォルダを作成し、その中に配置したファイルをビルドすることができるようです。
例えば、以下のように newprogram.rs を作成した場合は、`cargo build —bin newprogram` コマンドを実行することで new_program.exe をビルドすることができます。
Rust で MessageBoxW API を使用する
今回は Windows Crate を利用して、MessageBoxW API を Rust から呼び出すプログラムを作成してみます。
参考:Windows 用 Rust と windows クレート | Microsoft Learn
crates.io で公開されている windows crate を利用することで、MessageBoxW などの Windows API を Rust プログラムから簡単に利用できるようになります。
Crate とは、他のプログラム言語におけるライブラリのようなもので、Windows Crate のように crates.io で公開されている外部 Crate についても、Cargo.toml ファイルの [dependencies]
に定義を追加しておくことで、cargo コマンドがビルド時に自動的に依存関係を解決してくれるようです。
MessageBoxW API を使用するために Windows Crate を登録する
まずは、MessageBoxW API を使用するために Cargo.toml ファイルに以下の行を追記しました。
以下の記述では、crates.io に公開されている windows crate のバージョン 0.60.0 を依存関係として指定し、features オプションで Win32_UI_WindowsAndMessaging
を指定しています。
[dependencies]
windows = { version = "0.60.0", features = ["Win32_UI_WindowsAndMessaging"] }
この features オプションは、コンパイル時に特定の機能を有効化することを指定するために使用できるようです。
今回の場合は、MessageBoxW を含む Win32_UI_WindowsAndMessaging
の機能を有効化しています。
参考:[Rust] フィーチャーフラグの使い方 #Rust - Qiita
参考:windows::Win32::UI::WindowsAndMessaging - Rust
参考:MessageBoxExW in windows::Win32::UI::WindowsAndMessaging - Rust
MessageBoxW.rs を作成してモジュールをロードする
続いて、src/bin/
直下に MessageBoxW.rs ファイルを作成し、以下のコードを記述しました。
use windows::{
core::PCWSTR,
Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION},
};
fn to_wide_null(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn main() {
let caption = to_wide_null("Title");
let message = to_wide_null("This is my first WinAPI with Rust.");
unsafe {
MessageBoxW(
None,
PCWSTR(message.as_ptr()),
PCWSTR(caption.as_ptr()),
MB_OK | MB_ICONINFORMATION,
);
}
}
まず、先頭行の部分ですが、use で必要なコンポーネントのロードを宣言しています。
この例では、Windows Crate から core::PCWSTR
と Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION}
をロードすることを指示しています。
use windows::{
core::PCWSTR,
Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION},
};
core::PCWSTR
は、Windows API における Null 終端の UTF-16 文字列を指すポインタ型です。
また、Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION}
は、Win32::UI::WindowsAndMessaging
から MessageBoxW、MBOK、MBICONINFORMATION の 3 つをロードしています。
スマートに記述されているので逆に慣れないと読みづらいですが、実際には以下のように記述しても同じようにプログラムを実行することができました。
use windows::core::PCWSTR;
use windows::Win32::UI::WindowsAndMessaging::MessageBoxW;
use windows::Win32::UI::WindowsAndMessaging::MB_OK;
use windows::Win32::UI::WindowsAndMessaging::MB_ICONINFORMATION;
関数を定義する
続く以下の箇所では、to_wide_null
という関数を定義しています。
Rust では、fn <関数名>()
の形式で関数を定義するようです。->
以降の記述は関数の戻り値の型を指定しています。
つまり、以下のコードでは Vec<u16>
によってこの関数がベクタ型を返すことが示されているとわかります。
fn to_wide_null(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
実際に、関数内で実行されている s.encode_utf16().chain(std::iter::once(0)).collect()
では、&str
型の文字列スライス s
を UTF-16 エンコードしたイテレータに .chain(std::iter::once(0))
によって末尾に Null 文字(0) の要素を追加し、.collect()
によって指定されている Vec<u16>
のベクタとして返します。(わかりづらい!)
MessageBoxW API を呼び出す
最後に、以下の main 関数を使用して MessageBoxW API を発行しています。
fn main() {
let caption = to_wide_null("Title");
let message = to_wide_null("This is my first WinAPI with Rust.");
unsafe {
MessageBoxW(
None,
PCWSTR(message.as_ptr()),
PCWSTR(caption.as_ptr()),
MB_OK | MB_ICONINFORMATION,
);
}
}
ここではまず、to_wide_null
関数によって Null 終端のワイド文字列化した変数 caption と message を定義しています。
VSCode で見ると型をコード内に表示してくれてかなり読みやすいです。
続いて、unsafe ブロックの中で MessageBoxW を呼び出しています。
unsafe は、コンパイル時に強制される Rust のメモリ安全性を回避し、メモリ安全性がコンパイル時に強制されないコードを実行する場合に指定する記法のようです。
unsafe を使用する場面はいくつかあるようですが、今回は extern によりエクスポートされている unsafe な関数である MessageBoxW を実行するため、unsafe ブロックを使用する必要があります。
参考:Unsafe Rust - The Rust Programming Language
参考:MessageBoxW in windows_win::sys - Rust
試しに unsafe ブロックを外してコンパイルを行ってみたところ、コンパイラがちゃんとエラーを吐いてくれました。
簡単に文字列リテラルを UTF-16 ワイド文字列に変換する
ここまでのコードでは、文字列を MessageBoxW の引数として渡す UTF-16 文字列に変換するために to_wide_null
関数を使用していました。
しかし、有識者の方から、 w!
というマクロを使用するともっと簡単に文字列の変換ができるという情報を得たのでコードを書きなおしてみました。
書きなおしたコードは以下です。かなりすっきりしました。
windows::core::w!
マクロは対象の文字列を UTF-16 ワイド文字列に変換した Null 終端のによって windows::core::PCWSTR
によりワイド文字列を格納したベクタのポインタを取得する部分も不要になりました。
use windows::{
core::w,
Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION},
};
fn main() {
let caption = w!("Title");
let message = w!("This is my first WinAPI with Rust.");
unsafe {
MessageBoxW(
None,
message,
caption,
MB_OK | MB_ICONINFORMATION,
);
}
}
このコードをビルドして実行すると、元のコードと同じく MessageBoxW を使用できました。
プログラムを実行する
ここまででコードは完成しているため、cargo run --bin MessageBoxW
を実行することで Rust から MessageBoxW API を呼び出すことができます。
Windows Crate が便利なので思いの外簡単に API を実行することができました。
まとめ
これから Rust で色々な Windows API を使用するプログラムを実装してみようと思っているので、手始めに MessageBoxW を使用するまでの手順をまとめてみました。