ChatGPTの手を借りつつ、超簡単なWindowsネイティブアプリをRustでつくった

最近、おそらくは常駐させているなんかのアプリの影響で、急にウィンドウのフォーカスを奪われることがある。例えば、普通にVSCodeで何かを書いてる時、急にフォーカスが飛ぶと普通にびっくりするし、何度も繰り返されるとイライラする。

直接的な原因になりそうなアプリが思いつくわけでもなかったので、どうにかしてフォーカスを奪ってくるアプリを知る必要があると思った。

こういうニッチなケースに関しては、サクッとコードを書くのが良いとされているので、そういうコンソールアプリを作ろう!と思い立った。

WindowsのAPIをバリバリに叩くアプリだし、しかも今回の場合はフック先の関数とかも書かないといけなさそうなので気が重くなりそうなところだけど、今はChatGPTという対話型最高アプリが存在するので腰を上げることができた。

ChatGPTは枯れつつあるプログラミングの知識についてはかなり良いものを持っており、今回のような例に関してはまさに持って来いの話題なのでは?と考え、今回はこいつにスタートダッシュを共にしてもらい、アプリを完成させた。

github.com

以下のような感じで、フォーカスがあたっているアプリが変更されたらそれにフックしてアプリの概要をプリントする、という簡単なもの。

アプリの様子

リポジトリ用のディレクトリが作られ、最終更新が発生したタイミングまで全部で35分くらいしか使っておらず、とても素晴らしかったので良かった点を記事として残しておく。

前提として、自分は今回使われるスタックについてはプロレベルではないにせよすべて一度以上は使ったことがあり、かつプログラミングについての全般は慣れ親しんでいる。プログラミングができない初心者がChatGPTを使ってアプリを作り上げたという記事ではないことに注意。

全体像は 会話全文 を読んでもらえば良いが、良かった部分をいくつか抜き出す。

会話

まずは要件を伝えてみると、C#のコードを返してきた。C#で触るWin32のAPIやラッパーはかなり好感触なんだけど、C#を触るために最適な環境であろう Visual Studio を触りたくなさすぎる。純粋にIDEとしては動作が重すぎるし、依存の在り方が複雑だし、正直なところ、触るだけで気が滅入ってしまう類のプロダクトだと思う。

要件を伝えると、C#のコードが返ってきた

ので、今回はポータブル性に優れており、かつ現代的な構文やエコシステムを持ちながらも、C/C++と同レベルのレイヤーのコードを各ことができる、僕のイチオシ言語 Rust で進めてもらうことにした。すると、一発目からなんだか動きそうなコードを出してきた。

いい感じなんだけど、個人的には winapi よりも windows を使う方が好みなので、こちらを使うように変更する指示を出してみる。

(この下部に src/main.rs のコードあり)

現在のChatGPTのGPT-4のモデルは2023年6月までで知識が止まっているため、古いバージョンのクレートを提示してくるが、これは自分の方で新しいものになるように吸収していく。提示されたソースコードをコピペすると、まあ当然のようにビルドエラーが出る。インポートエラーは正しいものになるように解決したが、どうも GetWindowTextW の使い方が間違っている。そもそも、引数の数が違っており、ChatGPTの提示では3つ渡されているが、実際の引数は2つを期待している様子だった。

この場合、関数のシグネチャを直接教えてやると、それに従ったコードを提示するようになる。

この時点で充分動くコードができあがるが、実際に動かしてみたところ、タイトルが表示されるだけでは情報が足りないことに気づく。ここで要件を追加してみる。

なんか色々言ってるが、WindowsのAPIをうまく使ったコードが生成された。これを必要な部分だけコピペし、動かしてみる。VSCode側ではGitHub Copilotも動いたりしていい感じにできあがっていくが、一点だけ良くない点が生まれた。この時点で、既に指示から逸脱して自分流の書き方をいくつかしていたため、以下のように現在地点を伝えるつもりでソースコード全文を貼り付ける。

いい感じの解決策を提示してくれた。PROCESS_NAME_WIN320 に直されたのは情報が古い問題によるもので、これを治したければまた正しいシグネチャを教えることでどうにかなりそうだが、あえてそこまで教えてやる必要もなく、こちらで修正して完了させた。

また、リポジトリの名前も考えてもらった。自分はあまり気の利いた名前を考えるのが得意じゃないので、頻繁にChatGPTに頼っている。

今回は愚直な名前が返ってきた。いい感じ。GitHubにリポジトリをpushしておいて、作業終了。あとはこれを常駐させて、実際に犯人特定の手がかりになればよいな~というところ。

最終的な成果物は以下に存在する。(再掲)

github.com


こんな感じで、知ってることであっても大体最初のとっかかりはChatGPTに書いてもらうことが多い。これまではGoogleで検索し、それっぽい記事のそれっぽいコードをコピペしていたが、検索結果のノイズが激しくなってきたことと、GPT-4の精度の高さを見込んで最初からChatGPTを使うようになった。

特に今回のような「Windowsの特定のイベントフック( SetWinEventHook あたりのやつ )をRustで書く」というニッチなケースに対してもいい感じに、しかも日本語で提示してきた。以下に提示した event_callback 関数のインターフェイスと main 関数はほとんどコピペで作られている。event_callback の各引数や、unsafe extern "system" などのキーワードはある程度unsafe Rustの、しかもWindowsを相手取っていないとサッと出てこないコードだろうし、main 関数に至っては最初から最後まで一文字すら変更していない。実際、main 関数の書き方は自分が一番億劫に感じている部分でもあるので、これが一発で出てきてくれたのはとてもありがたいことだった。

unsafe extern "system" fn event_callback(
    _h_win_event_hook: HWINEVENTHOOK,
    _event: u32,
    hwnd: HWND,
    _id_object: i32,
    _id_child: i32,
    _id_event_thread: u32,
    _dwms_event_time: u32,
) {
    // snip
}

fn main() -> Result<()> {
    unsafe {
        let event_hook = SetWinEventHook(
            EVENT_SYSTEM_FOREGROUND,
            EVENT_SYSTEM_FOREGROUND,
            None,
            Some(event_callback),
            0,
            0,
            WINEVENT_OUTOFCONTEXT,
        );

        let mut msg: MSG = MSG::default();
        while GetMessageW(&mut msg, HWND(0), 0, 0).into() {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }

        UnhookWinEvent(event_hook);
    }
    Ok(())
}

WindowsのAPIを相手にするプログラミングは苦痛が多いことで知られているし、Rustの関数をWindowsが叩けるように渡す行為はさらに苦しいことが多いが、(もちろん要件の簡単さもあれど)テンプレートとしては最もすばらしいものを提示してくれたおかげで、実際に書きたいドメインロジック的な部分を超高速で仕上げることができた。とても嬉しい。