動機
私は Firefox のマウスジェスチャ拡張 Gesturefy を使用しています。しかし、WebExtensions の Content script は「設定」とか「about:newtab」のような chrome ページでは動作しません。なのでホイールジェスチャでタブをパラパラと切り替えているときに chrome ページでタブの切り替えが止まってしまったり、それらのタブがジェスチャで閉じられないということが発生します。「イラッ」とします。これをなんとかしましょう。Rust で。
どうするか
WebExtensions の Native Messaging と Windows のグローバルフックを使います。
流れとしては、
- 拡張から EXE を起動する。
- EXE は DLL をあらゆるプロセスに Injection してマウスのイベントを補足する。
- DLL は Firefox 上でのジェスチャを拡張に通知する。
- 拡張がジェスチャを受け取って、対応するコマンド(「戻る」とか「進む」とか)を実行する。
EXE と DLL を Rust で作ります。グローバルフックのための共有メモリ領域の定義は C++ 闇の力を借りました。図にすると下の通りです。
実装
私が詰まったところだけ抜粋で書きます。ソースコードの全体像はこちらを参照してください。
なぜか DLL と EXE は 32bit でビルドしないと Firefox 上でのマウスメッセージを補足できません。
DLL
グローバルフックを実現するには共有メモリ領域にフックプロシージャへのハンドルを保持する必要がありますが、Rust でのやり方が分かりませんでした。(メモリマップトファイルで可能?)
仕方なしに共有メモリ領域を C++ で定義して、それを Rust の世界に持ってくることにしました。
まずは C++ から。#pragma data_seg()
内に共有したい変数を定義します。マウスポインタの最後の位置も共有メモリ領域に確保しておきます。C++ としてコンパイルするのでextern "C"
を付けておかないとマングルされて Rust 側から見つけられなくなります。
# include <windows.h>
# pragma data_seg("SHARED")
extern "C" HHOOK gHook = NULL; // フックプロシージャハンドル
extern "C" LONG gLastX = 0;
extern "C" LONG gLastY = 0;
# pragma data_seg()
さらにリンカにSHARED
が共有メモリ領域であることを教える必要があります。Cargo.toml
があるフォルダに.cargo
フォルダを作って、その中にconfig
ファイルを作成します。またビルドした成果物は EXE 側に保存するようにしてます。
[build]
rustflags = ["-C", "link-arg=/SECTION:SHARED,RWS"]
target-dir = "../exe/target"
C++ のソースコードはcc
crate を使ってビルドしてやるので、build.rs
を作成します。
use cc;
fn main() {
cc::Build::new()
.cpp(true)
.file("src\\cpp\\shareddata.cpp")
.include("src")
.compile("shareddata");
}
続いて Rust。#[link(kind = "static")]
アトリビュートで C++ のオブジェクトも DLL 内に含めてしまいます。
use winapi::shared::windef::HHOOK;
use winapi::um::winnt::LONG;
use winapi::um::winuser::{
SetWindowsHookExW, UnhookWindowsHookEx, WH_MOUSE_LL
};
# [link(name = "shareddata", kind = "static")]
extern "C" {
static mut gHook: HHOOK;
static mut gLastX: LONG;
static mut gLastY: LONG;
}
# [no_mangle]
pub extern "C" fn sethook() -> bool {
unsafe {
gHook = SetWindowsHookExW(WH_MOUSE_LL, Some(hook_proc), gDll, 0);
if gHook.is_null() {
return false;
}
true
}
}
# [no_mangle]
pub extern "C" fn unhook() -> bool {
unsafe {
if !gHook.is_null() {
return UnhookWindowsHookEx(gHook) > 0;
}
false
}
}
これでcargo build
すれば、DLL が出来上がります。sethook
関数を EXE 側から呼び出せばグローバルフックができます。
EXE
続いて EXE 側の実装です。DLL の関数を呼び出すには#[link(kind = "dylib")]
アトリビュートを使用します。name
のところを"monkeyhook"
ではなくて、"monkeyhook.dll"
と書く必要があります。(ライブラリの名前がmonkeyhook.dll.lib
なので)
ウィンドウプロシージャでウィンドウが作成されたときにグローバルフックを開始して、ウィンドウが閉じられたらフックを解除します。また拡張からのメッセージを受信するためのループを別スレッドで回しています。
# [link(name = "monkeyhook.dll", kind = "dylib")]
extern "C" {
fn sethook() -> bool;
fn unhook() -> bool;
}
unsafe extern "system" fn win_proc(hwnd: HWND, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT {
match msg {
WM_CREATE => {
thread::spawn(|| event_loop(callback));
sethook();
}
WM_CLOSE => {
unhook();
DestroyWindow(hwnd);
}
WM_DESTROY => PostQuitMessage(0),
_ => return DefWindowProcW(hwnd, msg, wp, lp),
};
0
}
拡張
Native Messaging するにはちょっと面倒な手続きが必要です。
まず拡張のmanifest.json
にパーミッション追加。
{
// omit
"permissions": [
"<all_urls>",
"tabs",
"menus",
"nativeMessaging"
],
"applications": {
"gecko": {
"id": "monkeygestures@benki.jp"
}
}
}
拡張のマニフェストとは別にアプリケーションマニフェストを用意。"path"
は環境に合わせて書き換えてください。
{
"name": "MonkeyGestures",
"path": "/path/to/monkeygestures.exe",
"type": "stdio",
"allowed_extensions": [
"monkeygestures@benki.jp"
]
}
以下のレジストリキーにmonkeygestures.json
へのパスを追加。
HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\MonkeyGestures
最後にbackground.js
です。runtime.connectNative
で EXE が起動されます。DLL からのジェスチャはport.onMessage.addListener
で受け取ります。
ジェスチャが実行された場合はコンテキストメニューを表示しないようにmenus.onShown.addListener
で EXE 側に"suppressContextMenu"
メッセージを送ります。メッセージを受け取った EXE はコンテキストメニューのウィンドウハンドルを取得してコンテキストメニューを閉じます。
let port = browser.runtime.connectNative("MonkeyGestures");
let directionChain = [];
port.onMessage.addListener((direction) => {
// 省略
directionChain.push(direction);
});
browser.menus.onShown.addListener((info, tab) => {
if (directionChain.length > 0) {
port.postMessage("suppressContextMenu");
}
});
まとめ
- Rust でグローバルフックするための DLL を作ったよ
- C++ のコードもいっしょにビルドしたよ
- Rust で作った EXE から、DLL の関数を呼び出したよ
- 拡張と EXE/DLL 間でメッセージのやり取り(Native Messaging)したよ
- ソースコードの全体像はこちら
最初は Rust で書き始めたのだけれでも、これが全然動かなかったです。それで一旦 C++ でちゃんと動くEXE と DLL を作りました(Visual Stdio 2019 のデバッグ機能のおかげです)。それを Rust に写経していくという手法になりました。ちょっと回り道になったけど、とりあえず動くものができてよかった