🪶

ctrlc よりも簡単に Ctrl-C を検知する、小さな crate を作った

に公開

前提

CLI ツールを書いていると「Ctrl-C を検知したい」という場面がよくあります。

その度にこんな実装を書くわけですが、どうにも気持ち悪いとずっと思っていました。

main.rs
let flag = Arc::new(AtomicBool::new(false));
let f = flag.clone();

ctrlc::set_handler(move || {
    f.store(true, Ordering::SeqCst);
})?;

loop {
    if flag.load(Ordering::SeqCst) {
        println!("Ctrl-C detected");
        break;
    }

    // work...
}
  • 目的のわりに実装が Too Much
  • 内部実装のオーバーヘッドが気になる
  • Arc<AtomicBool> が嫌い

そこで ctrlc-tiny という「Ctrl-C が押されたかを知りたい」だけに特化した crate を作りました。

使い方

Cargo.toml
ctrlc-tiny = "0.1"
main.rs
ctrlc_tiny::init_ctrlc()?;

loop {
    if ctrlc_tiny::is_ctrlc_received() {
        println!("Ctrl-C detected");
        break;
    }

    // work...
}
  • init_ctrlc() でハンドラを登録
  • あとは is_ctrlc_received() をポーリングするだけ
  • AtomicBool や Arc といった、スレッドセーフな共有状態の管理を意識する必要がない

Ctrl-C を複数回検知したい時は

reset_ctrlc_received() という関数を用意しています。
こんな場合でも、共有状態の管理を意識する必要のないシンプルな実装が可能です。

main.rs
ctrlc_tiny::init_ctrlc()?;
let mut count = 0;

loop {
    if ctrlc_tiny::is_ctrlc_received() {
        ctrlc_tiny::reset_ctrlc_received();

        count += 1;
        println!("Detected Ctrl-C: {count}");

        if count == 10 {
            break;
        }
    }
}

実装の面白かったところ

今回の実装は、ほぼすべて C 言語で完結しており、Rust 側では FFI 経由で C 関数を呼び出しているだけです。

これまで低レイヤーでのシグナルハンドリングは触ってきませんでしたが、想像以上にクセがあり、なかなか楽しかったです。

特に、シグナルハンドラの中で使える変数やシステムコールはかなり限られており、async-signal-safeな実装にするためには、かなり気を使う必要があります。

今回は is_sigint_received に代入するだけなので、わかりやすく async-signal-safe なシグナルハンドラを実現できています。

sigint.c
volatile sig_atomic_t is_sigint_received = 0;

void handle_sigint(int signo)
{
    (void)signo;
    is_sigint_received = 1;
}

int init_sigint_handler()
{
    struct sigaction sa = {0};
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    return sigaction(SIGINT, &sa, NULL);
}

sig_atomic_t get_is_sigint_received()
{
    return is_sigint_received;
}
lib.rs
static INIT: Once = Once::new();

pub fn init_ctrlc() -> io::Result<()> {
    let mut result = Ok(());
    INIT.call_once(|| unsafe {
        if bindings::init_sigint_handler() != 0 {
            result = Err(io::Error::last_os_error());
        }
    });
    result
}

pub fn is_ctrlc_received() -> bool {
    unsafe { bindings::get_is_sigint_received() != 0 }
}

終わりに

正直なところ、Arc や AtomicBool、あるいは ctrlc クレート内部の実装について、パフォーマンス的に現実的な問題があるわけではありません。

個人的、主観的な気持ち悪さと、実装のちょっとした煩雑さの解消が今回の目的で、その点では満足できる仕上がりになったと思っています。

ご意見・改善案などあれば PR お待ちしています!

GitHubで編集を提案

Discussion