12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust であらゆるページで動くマウスジェスチャ拡張を作る

Last updated at Posted at 2020-05-16

動機

私は Firefox のマウスジェスチャ拡張 Gesturefy を使用しています。しかし、WebExtensions の Content script は「設定」とか「about:newtab」のような chrome ページでは動作しません。なのでホイールジェスチャでタブをパラパラと切り替えているときに chrome ページでタブの切り替えが止まってしまったり、それらのタブがジェスチャで閉じられないということが発生します。「イラッ」とします。これをなんとかしましょう。Rust で。

どうするか

WebExtensions の Native Messaging と Windows のグローバルフックを使います。

流れとしては、

  • 拡張から EXE を起動する。
  • EXE は DLL をあらゆるプロセスに Injection してマウスのイベントを補足する。
  • DLL は Firefox 上でのジェスチャを拡張に通知する。
  • 拡張がジェスチャを受け取って、対応するコマンド(「戻る」とか「進む」とか)を実行する。

EXE と DLL を Rust で作ります。グローバルフックのための共有メモリ領域の定義は C++ 闇の力を借りました。図にすると下の通りです。
monkeygestures.png

実装

私が詰まったところだけ抜粋で書きます。ソースコードの全体像はこちらを参照してください。
なぜか DLL と EXE は 32bit でビルドしないと Firefox 上でのマウスメッセージを補足できません。

DLL

グローバルフックを実現するには共有メモリ領域にフックプロシージャへのハンドルを保持する必要がありますが、Rust でのやり方が分かりませんでした。(メモリマップトファイルで可能?)
仕方なしに共有メモリ領域を C++ で定義して、それを Rust の世界に持ってくることにしました。

まずは C++ から。#pragma data_seg()内に共有したい変数を定義します。マウスポインタの最後の位置も共有メモリ領域に確保しておきます。C++ としてコンパイルするのでextern "C"を付けておかないとマングルされて Rust 側から見つけられなくなります。

/src/cpp/shareddata.cpp
# 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 側に保存するようにしてます。

/.cargo/config
[build]
rustflags = ["-C", "link-arg=/SECTION:SHARED,RWS"]
target-dir = "../exe/target"

C++ のソースコードはcccrate を使ってビルドしてやるので、build.rsを作成します。

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 内に含めてしまいます。

lib.rs
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なので)

ウィンドウプロシージャでウィンドウが作成されたときにグローバルフックを開始して、ウィンドウが閉じられたらフックを解除します。また拡張からのメッセージを受信するためのループを別スレッドで回しています。

main.rs
# [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にパーミッション追加。

manifest.json
{
    // omit
    "permissions": [
        "<all_urls>",
        "tabs",
        "menus",
        "nativeMessaging"
    ],
    "applications": {
        "gecko": {
            "id": "monkeygestures@benki.jp"
        }
    }
}

拡張のマニフェストとは別にアプリケーションマニフェストを用意。"path"は環境に合わせて書き換えてください。

monkeygestures.json
{
    "name": "MonkeyGestures",
    "path": "/path/to/monkeygestures.exe",
    "type": "stdio",
    "allowed_extensions": [
        "monkeygestures@benki.jp"
    ]
}

以下のレジストリキーにmonkeygestures.jsonへのパスを追加。

HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\MonkeyGestures

regedit.png

最後にbackground.jsです。runtime.connectNativeで EXE が起動されます。DLL からのジェスチャはport.onMessage.addListenerで受け取ります。
ジェスチャが実行された場合はコンテキストメニューを表示しないようにmenus.onShown.addListenerで EXE 側に"suppressContextMenu"メッセージを送ります。メッセージを受け取った EXE はコンテキストメニューのウィンドウハンドルを取得してコンテキストメニューを閉じます。

background.js
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 に写経していくという手法になりました。ちょっと回り道になったけど、とりあえず動くものができてよかった:blush:

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?