diff --git a/README.md b/README.md index 7e4d40d..4b884dd 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,79 @@ # novonotes_run_loop -プラットフォーム独立なランループインターフェースを提供する Rust クレート。 -[irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop) をベースに、DLL 環境での安全性やエラーハンドリングなどを強化したフォークです。 +A platform-independent run loop interface for Rust. +A fork of [irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop) with enhanced safety and error handling for DLL environments. -## 目的 +> Japanese version: [README_JA.md](./README_JA.md) -オーディオプラグイン開発における非同期タスク管理での使用を想定。 -ホストアプリケーションのメインスレッドをブロックすることなく、そのスレッドのランループへのアクセスを提供する。 -これにより、プラグイン起点でメインスレッドのタスクを起動・スケジューリングすることが可能になる。 +## Purpose -## 特徴 +Designed for async task management in audio plugin development. +Provides access to the host application's main thread run loop without blocking it, +enabling plugins to schedule and launch tasks on the main thread. -- **マルチプラットフォーム対応**: iOS/macOS、Android、Linux、Windows のネイティブランループを統一 API で操作 -- **非同期タスク管理**: Rust 標準の async/await パターンをサポート -- **スレッド間通信**: 安全なメッセージパッシング機構 -- **DLL/オーディオプラグイン対応**: 他社アプリケーションにリンクされた DLL で動作するユースケースをサポート +## Features -## 基本的な使い方 +- **Multi-platform**: Unified API over native run loops on iOS/macOS, Android, Linux, and Windows +- **Async task management**: Supports Rust's standard `async`/`await` patterns +- **Cross-thread communication**: Safe message-passing mechanism +- **DLL / audio plugin support**: Handles use cases where the library runs inside a DLL linked into a third-party application -### 初期化 +## Basic Usage + +### Initialization ```rust use novonotes_run_loop::{RunLoop, JoinError}; -// アプリケーション/DLL の初期化処理で実行 -RunLoop::init().expect("RunLoop の初期化に失敗"); +// Call during application/DLL initialization +RunLoop::init().expect("Failed to initialize RunLoop"); -// 現在のスレッドのRunLoopを取得 +// Get the RunLoop for the current thread let run_loop = RunLoop::current(); -// アプリケーション/DLL の終了処理で実行 +// Call during application/DLL teardown RunLoop::deinit(); ``` -### タスクのスケジューリング +### Scheduling Tasks ```rust use std::time::Duration; let run_loop = RunLoop::current(); -// 10秒後の実行をスケジュール +// Schedule execution after 10 seconds let handle = run_loop.schedule(Duration::from_secs(10), || { - println!("10秒経過しました"); + println!("10 seconds have passed"); }); -// handleをドロップするとタイマーがキャンセルされる -// キャンセルを防ぐには detach() を使用 +// Dropping the handle cancels the timer. +// Use detach() to prevent cancellation. handle.detach(); ``` -### 非同期タスクの実行 +### Running Async Tasks ```rust -// タスクをスポーンして結果を待機 +// Spawn a task and await its result let handle = run_loop.spawn(async { - // 非同期処理 + // Async work RunLoop::current().delay(Duration::from_secs(1)).await; 42 }); -// 結果の取得(エラーハンドリング付き) +// Retrieve the result with error handling match handle.await { - Ok(value) => println!("結果: {}", value), // `結果: 42` が出力されるはず。 - Err(JoinError::Aborted) => println!("タスクが中断されました"), - Err(JoinError::Panic(_)) => println!("タスクがパニックしました"), + Ok(value) => println!("Result: {}", value), // Should print `Result: 42` + Err(JoinError::Aborted) => println!("Task was aborted"), + Err(JoinError::Panic(_)) => println!("Task panicked"), } ``` -### スレッド間通信 +### Cross-Thread Communication -RunLoop は初期化されたスレッドを RunLoop スレッドとしてマークします。 -別スレッドから RunLoop スレッドへのコールバック送信は `RunLoop::sender()` を使用します。 +`RunLoop::init()` marks the current thread as the run loop thread. +Use `RunLoop::sender()` to send callbacks from other threads to the run loop thread. ```rust use std::thread; @@ -79,52 +81,51 @@ use std::thread; fn main() { assert!(RunLoop::is_run_loop_thread()); - // 別スレッドからRunLoopスレッドにコールバックを送信 + // Send a callback from another thread to the run loop thread thread::spawn(move || { let sender = RunLoop::sender(); - // 送信されたコールバックは RunLoop スレッドで非同期実行されます。 + // Sent callbacks are executed asynchronously on the run loop thread. sender.send(|| { assert!(RunLoop::is_run_loop_thread()); - println!("RunLoopスレッドで実行"); + println!("Executing on run loop thread"); }); }); } ``` -## プラットフォーム別の実装 +## Platform Implementations -| プラットフォーム | 基盤技術 | 特徴 | -| ---------------- | ------------------ | ------------------------------------------------- | -| iOS/macOS | CFRunLoop | Core Foundation ベース、カスタム RunLoopMode 使用 | -| Android | ALooper | NDK の ALooper、timerfd でタイマー実装 | -| Linux | GMainContext | GLib/GTK 統合、g_timeout_source でタイマー | -| Windows | Win32 Message Loop | 隠しウィンドウでメッセージ処理 | +| Platform | Underlying Technology | Notes | +| --------- | --------------------- | -------------------------------------------------------- | +| iOS/macOS | CFRunLoop | Core Foundation based, uses a custom RunLoopMode | +| Android | ALooper | NDK ALooper, timer implemented with timerfd | +| Linux | GMainContext | GLib/GTK integration, timer via g_timeout_source | +| Windows | Win32 Message Loop | Message processing via a hidden window | -## 動作モデル +## Execution Model -`init()` は現在のスレッドにネイティブループの基盤への参照を取得(存在しない場合は新規作成)します。スタンドアロンアプリでは自前での `run()` 呼び出しによって、ランループを駆動する必要があります。プラグイン環境ではホストが既にループを駆動しているため、`run()` の呼び出しは不要です。その後のコールバックやタイマー登録は、誰がループを駆動しているかに関係なく同じように動作します。 +`init()` acquires a reference to the native loop infrastructure on the current thread (creating one if none exists). In standalone applications, you must call `run()` yourself to drive the loop. In plugin environments the host already drives the loop, so `run()` is unnecessary. Callbacks and timer registrations behave identically regardless of who drives the loop. -| パターン | `run()` | 誰がループを回すか | -|---|---|---| -| スタンドアロンアプリ | 呼ぶ | `run()` 自身 | -| プラグイン(CLAP/VST3 等) | 呼ばない | ホスト(DAW)の既存ループ | +| Pattern | `run()` | Who drives the loop | +|-----------------------|---------|---------------------------| +| Standalone app | call | `run()` itself | +| Plugin (CLAP/VST3 …) | skip | Host (DAW) existing loop | -## irondash_run_loop との主な違い +## Differences from irondash_run_loop -1. **DLL セーフティ**: thread-local ストレージを使用しないように変更。初期化・終了処理を見直し。複数 DLL 間での Win32 の Window Class 名や CFRunLoop の RunLoopMode 名の衝突を回避。 -2. **明示的な abort() メソッド**: タスクの制御された中断が可能 -3. **パニックリカバリ**: タスク内のパニックをキャッチして報告 +1. **DLL safety**: Removed thread-local storage. Revised initialization/teardown. Avoids name collisions for Win32 Window Class names and CFRunLoop RunLoopMode names across multiple DLLs. +2. **Explicit `abort()` method**: Allows controlled task cancellation. +3. **Panic recovery**: Catches and reports panics that occur inside tasks. -## テスト +## Testing -run_loop のテストヘルパーとテストハーネスの使い方については、[テストガイド](docs/testing.md)を参照してください。 +For information on using the test helpers and test harness, see the [Testing Guide](docs/testing.md). -## ライセンス +## License -MIT License(オリジナルプロジェクトと同じ) +MIT License (same as the original project) ## Upstream -このリポジトリは [`irondash`](https://github.com/irondash/irondash) の -`run_loop` クレートをベースにしたフォークです。 +This repository is a fork of the `run_loop` crate from [`irondash`](https://github.com/irondash/irondash). diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 0000000..7e4d40d --- /dev/null +++ b/README_JA.md @@ -0,0 +1,130 @@ +# novonotes_run_loop + +プラットフォーム独立なランループインターフェースを提供する Rust クレート。 +[irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop) をベースに、DLL 環境での安全性やエラーハンドリングなどを強化したフォークです。 + +## 目的 + +オーディオプラグイン開発における非同期タスク管理での使用を想定。 +ホストアプリケーションのメインスレッドをブロックすることなく、そのスレッドのランループへのアクセスを提供する。 +これにより、プラグイン起点でメインスレッドのタスクを起動・スケジューリングすることが可能になる。 + +## 特徴 + +- **マルチプラットフォーム対応**: iOS/macOS、Android、Linux、Windows のネイティブランループを統一 API で操作 +- **非同期タスク管理**: Rust 標準の async/await パターンをサポート +- **スレッド間通信**: 安全なメッセージパッシング機構 +- **DLL/オーディオプラグイン対応**: 他社アプリケーションにリンクされた DLL で動作するユースケースをサポート + +## 基本的な使い方 + +### 初期化 + +```rust +use novonotes_run_loop::{RunLoop, JoinError}; + +// アプリケーション/DLL の初期化処理で実行 +RunLoop::init().expect("RunLoop の初期化に失敗"); + +// 現在のスレッドのRunLoopを取得 +let run_loop = RunLoop::current(); + +// アプリケーション/DLL の終了処理で実行 +RunLoop::deinit(); +``` + +### タスクのスケジューリング + +```rust +use std::time::Duration; + +let run_loop = RunLoop::current(); + +// 10秒後の実行をスケジュール +let handle = run_loop.schedule(Duration::from_secs(10), || { + println!("10秒経過しました"); +}); + +// handleをドロップするとタイマーがキャンセルされる +// キャンセルを防ぐには detach() を使用 +handle.detach(); +``` + +### 非同期タスクの実行 + +```rust +// タスクをスポーンして結果を待機 +let handle = run_loop.spawn(async { + // 非同期処理 + RunLoop::current().delay(Duration::from_secs(1)).await; + 42 +}); + +// 結果の取得(エラーハンドリング付き) +match handle.await { + Ok(value) => println!("結果: {}", value), // `結果: 42` が出力されるはず。 + Err(JoinError::Aborted) => println!("タスクが中断されました"), + Err(JoinError::Panic(_)) => println!("タスクがパニックしました"), +} +``` + +### スレッド間通信 + +RunLoop は初期化されたスレッドを RunLoop スレッドとしてマークします。 +別スレッドから RunLoop スレッドへのコールバック送信は `RunLoop::sender()` を使用します。 + +```rust +use std::thread; + +fn main() { + assert!(RunLoop::is_run_loop_thread()); + + // 別スレッドからRunLoopスレッドにコールバックを送信 + thread::spawn(move || { + let sender = RunLoop::sender(); + // 送信されたコールバックは RunLoop スレッドで非同期実行されます。 + sender.send(|| { + assert!(RunLoop::is_run_loop_thread()); + println!("RunLoopスレッドで実行"); + }); + }); +} +``` + + +## プラットフォーム別の実装 + +| プラットフォーム | 基盤技術 | 特徴 | +| ---------------- | ------------------ | ------------------------------------------------- | +| iOS/macOS | CFRunLoop | Core Foundation ベース、カスタム RunLoopMode 使用 | +| Android | ALooper | NDK の ALooper、timerfd でタイマー実装 | +| Linux | GMainContext | GLib/GTK 統合、g_timeout_source でタイマー | +| Windows | Win32 Message Loop | 隠しウィンドウでメッセージ処理 | + +## 動作モデル + +`init()` は現在のスレッドにネイティブループの基盤への参照を取得(存在しない場合は新規作成)します。スタンドアロンアプリでは自前での `run()` 呼び出しによって、ランループを駆動する必要があります。プラグイン環境ではホストが既にループを駆動しているため、`run()` の呼び出しは不要です。その後のコールバックやタイマー登録は、誰がループを駆動しているかに関係なく同じように動作します。 + +| パターン | `run()` | 誰がループを回すか | +|---|---|---| +| スタンドアロンアプリ | 呼ぶ | `run()` 自身 | +| プラグイン(CLAP/VST3 等) | 呼ばない | ホスト(DAW)の既存ループ | + +## irondash_run_loop との主な違い + +1. **DLL セーフティ**: thread-local ストレージを使用しないように変更。初期化・終了処理を見直し。複数 DLL 間での Win32 の Window Class 名や CFRunLoop の RunLoopMode 名の衝突を回避。 +2. **明示的な abort() メソッド**: タスクの制御された中断が可能 +3. **パニックリカバリ**: タスク内のパニックをキャッチして報告 + +## テスト + +run_loop のテストヘルパーとテストハーネスの使い方については、[テストガイド](docs/testing.md)を参照してください。 + +## ライセンス + +MIT License(オリジナルプロジェクトと同じ) + +## Upstream + +このリポジトリは [`irondash`](https://github.com/irondash/irondash) の +`run_loop` クレートをベースにしたフォークです。 diff --git a/benches/latency.rs b/benches/latency.rs index b2cd1af..a2d9649 100644 --- a/benches/latency.rs +++ b/benches/latency.rs @@ -86,11 +86,11 @@ fn bench_sender_cross_thread_latency(c: &mut Criterion) { BenchmarkId::from_parameter(format!("{}_threads", thread_count)), thread_count, |b, &thread_count| { - // メインスレッドのrunloop初期化 + // Initialize the run loop on the main thread RunLoop::init().unwrap(); let main_run_loop = RunLoop::current(); - // 各スレッドからの送信を測定 + // Measure send latency from each thread b.iter(|| { let barrier = Arc::new(Barrier::new(thread_count + 1)); let completed = Arc::new(Mutex::new(0)); @@ -122,10 +122,10 @@ fn bench_sender_cross_thread_latency(c: &mut Criterion) { handles.push(handle); } - // 全スレッドを同時にスタート + // Start all threads simultaneously barrier.wait(); - // メインスレッドでイベントを処理 + // Process events on the main thread main_run_loop.run(); for handle in handles { @@ -145,7 +145,7 @@ fn bench_sender_cross_thread_latency(c: &mut Criterion) { } fn bench_sender_send_and_wait_latency(c: &mut Criterion) { - // 別スレッドでRunLoopを実行 + // Run the RunLoop on a background thread let (ready_tx, ready_rx) = mpsc::channel(); let (sender_tx, sender_rx) = mpsc::channel::(); let running = Arc::new(Mutex::new(true)); @@ -157,7 +157,7 @@ fn bench_sender_send_and_wait_latency(c: &mut Criterion) { let sender = RunLoop::sender(); sender_tx.send(sender).unwrap(); - // 定期的に停止フラグをチェック + // Periodically check the stop flag let running_check = running_clone.clone(); run_loop.spawn(async move { ready_tx.send(()).unwrap(); @@ -258,7 +258,7 @@ fn bench_schedule_with_delay(c: &mut Criterion) { }); handle.detach(); - // タイムアウト用のタイマーも設定 + // Also set a timeout timer let mut timeout_handle = run_loop.schedule(Duration::from_millis(delay_ms + 100), || { RunLoop::current().stop(); diff --git a/docs/maintainers.md b/docs/maintainers.md index e13e3ef..4fe2573 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -1,111 +1,115 @@ -# run_loop 保守・設計メモ +# run_loop Maintainer & Design Notes -この文書は **保守・移植を行う人向け** です。利用者向けのドキュメントは -[README.md](../README.md) と [lib.rs のクレートドキュメント](../src/lib.rs) を参照してください。 +This document is intended for **maintainers and porters**. For user-facing documentation, see +[README.md](../README.md) and the [lib.rs crate docs](../src/lib.rs). + +> Japanese version: [maintainers_JA.md](./maintainers_JA.md) --- -## upstream との関係 +## Relationship with upstream -[irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop) をベースに -フォークしたクレートですが、**upstream との継続的な sync は行わない方針**です。 +This crate is forked from [irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop), +but **ongoing sync with upstream is intentionally not performed**. -irondash はマルチプラットフォーム Flutter プラグイン開発を目的としていますが、 -このクレートはオーディオプラグイン(CLAP/VST3)固有の要件(DLL セーフティ、 -DAW ホストへの panic 伝播防止など)に最適化する方向で独自に進化させます。 +irondash targets multi-platform Flutter plugin development, whereas this crate is evolved +independently to optimize for audio plugin (CLAP/VST3) specific requirements such as DLL safety +and preventing panic propagation into the DAW host. --- -## irondash からの主な差分 +## Key differences from irondash -| 変更 | 理由 | +| Change | Reason | |---|---| -| thread-local ストレージを廃止、グローバル singleton に変更 | DLL unload 時の TLS destructor 問題を回避(後述) | -| `init()` / `deinit()` の参照カウント方式 | CLAP の `init` / `deinit` と対応させるため(後述) | -| Win32 の Window Class 名、CFRunLoop の RunLoopMode 名を固有名に変更 | 同一プロセスに複数 DLL が読み込まれたときの名前衝突を回避 | -| `abort()` メソッドを追加 | タスクの制御された中断が可能に | -| task 内 panic を `catch_unwind` でキャッチ | DAW ホストを巻き込まないため(後述) | -| `block_on()` を追加 | CLAP GUI スレッドで同期的に Future を待機するため(後述) | +| Removed thread-local storage; replaced with a global singleton | Avoids TLS destructor issues on DLL unload (see below) | +| Reference-counted `init()` / `deinit()` | Maps to CLAP `init` / `deinit` lifecycle (see below) | +| Unique Win32 Window Class name and CFRunLoop RunLoopMode name | Prevents name collisions when multiple DLLs are loaded in the same process | +| Added `abort()` method | Enables controlled task cancellation | +| Panic inside a task is caught with `catch_unwind` | Prevents taking down the DAW host (see below) | +| Added `block_on()` | Allows synchronously awaiting a Future on the CLAP GUI thread (see below) | --- -## singleton 制約 +## Singleton constraint -プロセス内で run loop スレッドは **常に 1 本だけ** という制約があります。 +There is a hard constraint that **only one run loop thread may exist in the process at any time**. -Darwin の `CFRunLoop` や Linux の `GMainContext` は「現在のスレッドの run loop」 -というスレッドローカルな概念を前提とした API です。複数スレッドがそれぞれ -run loop を持つ設計も可能ですが、オーディオプラグインでは「GUI スレッド = run loop -スレッド」という対応で十分であり、複雑さを増やすメリットがありません。 +`CFRunLoop` on Darwin and `GMainContext` on Linux are APIs that assume a thread-local concept of +"the current thread's run loop". It would be possible to design a system where multiple threads each +have their own run loop, but for audio plugins the mapping of "GUI thread = run loop thread" is +sufficient, and adding that complexity brings no benefit. -テスト環境では任意のスレッドを run loop スレッドに指定できるため、 -必ず `#[serial_test::serial]` で直列化してください。 +In test environments any thread can be designated the run loop thread, so always serialize tests +with `#[serial_test::serial]`. --- -## `init` / `deinit` の設計 +## `init` / `deinit` design -参照カウント方式(`INIT_COUNT: AtomicUsize`)を採用しています。 +A reference-counting scheme (`INIT_COUNT: AtomicUsize`) is used. -CLAP / VST3 では `InitDll` / `ExitDll`(または `init` / `deinit`)が -**複数回呼ばれることがある**ためです(複数プラグインが同一 DLL を参照する場合など)。 -`INIT_COUNT` が 0→1 になったときに実際の初期化、1→0 になったときにクリーンアップが走ります。 +CLAP / VST3 can call `InitDll` / `ExitDll` (or `init` / `deinit`) **multiple times** +(e.g., when multiple plugins reference the same DLL). +Actual initialization runs when `INIT_COUNT` transitions from 0→1; cleanup runs when it +transitions from 1→0. -誤用パターン: -- `deinit()` を `init()` より多く呼ぶ → カウントがアンダーフローし、次の `init()` で - クリーンアップ済みインスタンスを参照するリスクがあります(`fetch_sub` の wrap-around のため検出が困難)。 -- `deinit()` を呼ばずに DLL がアンロードされる → `RunLoopInner::drop` でフォールバック処理が - 走りますが、ベストエフォートです。shutdown パスでは panic を起こさないことが重要です。 +Misuse patterns: +- Calling `deinit()` more times than `init()` → The count underflows, risking a reference to a + cleaned-up instance on the next `init()` (hard to detect due to `fetch_sub` wrap-around). +- DLL unloaded without calling `deinit()` → `RunLoopInner::drop` runs a best-effort fallback, but + it is best-effort only. It is critical that the shutdown path does not panic. --- -## TLS を避けている理由 +## Why TLS is avoided -thread-local ストレージは DLL unload 時の destructor の順序・タイミングの制御が難しいです。 -特に Windows では `DLL_THREAD_DETACH` / `DLL_PROCESS_DETACH` の順序がホスト依存であり、 -他の TLS にアクセスする destructor がクラッシュする既知の問題があります。 +Thread-local storage makes it difficult to control the order and timing of destructors on DLL +unload. On Windows in particular, the ordering of `DLL_THREAD_DETACH` / `DLL_PROCESS_DETACH` is +host-dependent, and destructors that access other TLS values are a known source of crashes. -また、同一プロセス内で別 DLL やホスト側コードが同じスレッドを使い続ける状況では、 -TLS に保持していた値がアンロード済み DLL 側のコード・データを参照してしまう危険があります。 +Additionally, in scenarios where the same thread is shared between a different DLL or the host +code, values held in TLS may end up referencing code or data from the already-unloaded DLL. -そのため、`RUN_LOOP_INSTANCE` と `RUN_LOOP_THREAD_ID` は `static` な -グローバル変数(`Mutex` でガード)として保持しています。 +For these reasons `RUN_LOOP_INSTANCE` and `RUN_LOOP_THREAD_ID` are held as `static` global +variables (guarded by `Mutex`). --- -## `block_on` の意図 +## Intent of `block_on` -`pollster::block_on` などの外部 executor は **run loop を駆動しません**。 -そのため、`spawn` で投入したタスクが完了するまで待ちたい場合、外部 executor を使うと -デッドロックします。 +External executors such as `pollster::block_on` **do not drive the run loop**. +Therefore, if you want to wait for a task submitted via `spawn` to complete, using an external +executor will deadlock. -`RunLoop::block_on` はプラットフォーム固有のポーリング(`platform_run_loop.poll_once`)を -回し続けながら Future を poll します。これにより、待機対象の Future が run loop 上の -別タスクの完了に依存していても正しく進行できます。 +`RunLoop::block_on` continuously calls the platform-specific poll (`platform_run_loop.poll_once`) +while polling the Future. This allows the Future being awaited to make progress even when it +depends on another task completing on the run loop. -ネストした `block_on` は `BLOCK_ON_ACTIVE` フラグで検出し、パニックさせます(再入によるデッドロックを防ぐため)。 +Nested `block_on` calls are detected by the `BLOCK_ON_ACTIVE` flag and will panic (to prevent +deadlocks from re-entrancy). --- -## platform backend の責務 +## Responsibilities of the platform backend -各プラットフォームの backend(`src/platform/`)は以下を実装します: +Each platform backend (`src/platform/`) implements the following: -- `PlatformRunLoop` — run loop の作成・破棄・ポーリング(`poll_once`) -- `PlatformRunLoopSender` — 他スレッドからコールバックをキューに投入 -- `PollSession` — `block_on` 内でのポーリング状態管理 +- `PlatformRunLoop` — creation, teardown, and polling of the run loop (`poll_once`) +- `PlatformRunLoopSender` — enqueuing callbacks from other threads +- `PollSession` — polling state management inside `block_on` -新しいプラットフォームを追加する場合は `src/platform/mod.rs` の -`cfg` 分岐と `PollSession` の実装を参照してください。 +When adding a new platform, refer to the `cfg` branches in `src/platform/mod.rs` and the +`PollSession` implementation. --- -## 変更時の注意 +## Change guidelines -- **shutdown パスで panic しない**: DAW ホストを巻き込みます。`catch_unwind` で囲むか、 - panic が起きない実装にしてください。 -- **main thread 判定ロジックを軽率に変えない**: `RUN_LOOP_THREAD_ID` の取得・比較は - 複数箇所で行われており、変更すると `sender()`、`current()`、`is_run_loop_thread()` の - 整合性が崩れます。 -- **Win32 の Window Class 名・CFRunLoop の RunLoopMode 名はユニークに保つ**: - `irondash` や他ライブラリとの名前衝突を防ぐため、クレート固有のプレフィックスを維持してください。 +- **Do not panic in the shutdown path**: It will take down the DAW host. Wrap with `catch_unwind` + or ensure the implementation cannot panic. +- **Do not carelessly change the main-thread detection logic**: `RUN_LOOP_THREAD_ID` is acquired + and compared in multiple places; changing it will break the consistency of `sender()`, + `current()`, and `is_run_loop_thread()`. +- **Keep Win32 Window Class names and CFRunLoop RunLoopMode names unique**: Maintain the + crate-specific prefix to prevent name collisions with `irondash` and other libraries. diff --git a/docs/maintainers_JA.md b/docs/maintainers_JA.md new file mode 100644 index 0000000..e13e3ef --- /dev/null +++ b/docs/maintainers_JA.md @@ -0,0 +1,111 @@ +# run_loop 保守・設計メモ + +この文書は **保守・移植を行う人向け** です。利用者向けのドキュメントは +[README.md](../README.md) と [lib.rs のクレートドキュメント](../src/lib.rs) を参照してください。 + +--- + +## upstream との関係 + +[irondash_run_loop](https://github.com/irondash/irondash/tree/main/run_loop) をベースに +フォークしたクレートですが、**upstream との継続的な sync は行わない方針**です。 + +irondash はマルチプラットフォーム Flutter プラグイン開発を目的としていますが、 +このクレートはオーディオプラグイン(CLAP/VST3)固有の要件(DLL セーフティ、 +DAW ホストへの panic 伝播防止など)に最適化する方向で独自に進化させます。 + +--- + +## irondash からの主な差分 + +| 変更 | 理由 | +|---|---| +| thread-local ストレージを廃止、グローバル singleton に変更 | DLL unload 時の TLS destructor 問題を回避(後述) | +| `init()` / `deinit()` の参照カウント方式 | CLAP の `init` / `deinit` と対応させるため(後述) | +| Win32 の Window Class 名、CFRunLoop の RunLoopMode 名を固有名に変更 | 同一プロセスに複数 DLL が読み込まれたときの名前衝突を回避 | +| `abort()` メソッドを追加 | タスクの制御された中断が可能に | +| task 内 panic を `catch_unwind` でキャッチ | DAW ホストを巻き込まないため(後述) | +| `block_on()` を追加 | CLAP GUI スレッドで同期的に Future を待機するため(後述) | + +--- + +## singleton 制約 + +プロセス内で run loop スレッドは **常に 1 本だけ** という制約があります。 + +Darwin の `CFRunLoop` や Linux の `GMainContext` は「現在のスレッドの run loop」 +というスレッドローカルな概念を前提とした API です。複数スレッドがそれぞれ +run loop を持つ設計も可能ですが、オーディオプラグインでは「GUI スレッド = run loop +スレッド」という対応で十分であり、複雑さを増やすメリットがありません。 + +テスト環境では任意のスレッドを run loop スレッドに指定できるため、 +必ず `#[serial_test::serial]` で直列化してください。 + +--- + +## `init` / `deinit` の設計 + +参照カウント方式(`INIT_COUNT: AtomicUsize`)を採用しています。 + +CLAP / VST3 では `InitDll` / `ExitDll`(または `init` / `deinit`)が +**複数回呼ばれることがある**ためです(複数プラグインが同一 DLL を参照する場合など)。 +`INIT_COUNT` が 0→1 になったときに実際の初期化、1→0 になったときにクリーンアップが走ります。 + +誤用パターン: +- `deinit()` を `init()` より多く呼ぶ → カウントがアンダーフローし、次の `init()` で + クリーンアップ済みインスタンスを参照するリスクがあります(`fetch_sub` の wrap-around のため検出が困難)。 +- `deinit()` を呼ばずに DLL がアンロードされる → `RunLoopInner::drop` でフォールバック処理が + 走りますが、ベストエフォートです。shutdown パスでは panic を起こさないことが重要です。 + +--- + +## TLS を避けている理由 + +thread-local ストレージは DLL unload 時の destructor の順序・タイミングの制御が難しいです。 +特に Windows では `DLL_THREAD_DETACH` / `DLL_PROCESS_DETACH` の順序がホスト依存であり、 +他の TLS にアクセスする destructor がクラッシュする既知の問題があります。 + +また、同一プロセス内で別 DLL やホスト側コードが同じスレッドを使い続ける状況では、 +TLS に保持していた値がアンロード済み DLL 側のコード・データを参照してしまう危険があります。 + +そのため、`RUN_LOOP_INSTANCE` と `RUN_LOOP_THREAD_ID` は `static` な +グローバル変数(`Mutex` でガード)として保持しています。 + +--- + +## `block_on` の意図 + +`pollster::block_on` などの外部 executor は **run loop を駆動しません**。 +そのため、`spawn` で投入したタスクが完了するまで待ちたい場合、外部 executor を使うと +デッドロックします。 + +`RunLoop::block_on` はプラットフォーム固有のポーリング(`platform_run_loop.poll_once`)を +回し続けながら Future を poll します。これにより、待機対象の Future が run loop 上の +別タスクの完了に依存していても正しく進行できます。 + +ネストした `block_on` は `BLOCK_ON_ACTIVE` フラグで検出し、パニックさせます(再入によるデッドロックを防ぐため)。 + +--- + +## platform backend の責務 + +各プラットフォームの backend(`src/platform/`)は以下を実装します: + +- `PlatformRunLoop` — run loop の作成・破棄・ポーリング(`poll_once`) +- `PlatformRunLoopSender` — 他スレッドからコールバックをキューに投入 +- `PollSession` — `block_on` 内でのポーリング状態管理 + +新しいプラットフォームを追加する場合は `src/platform/mod.rs` の +`cfg` 分岐と `PollSession` の実装を参照してください。 + +--- + +## 変更時の注意 + +- **shutdown パスで panic しない**: DAW ホストを巻き込みます。`catch_unwind` で囲むか、 + panic が起きない実装にしてください。 +- **main thread 判定ロジックを軽率に変えない**: `RUN_LOOP_THREAD_ID` の取得・比較は + 複数箇所で行われており、変更すると `sender()`、`current()`、`is_run_loop_thread()` の + 整合性が崩れます。 +- **Win32 の Window Class 名・CFRunLoop の RunLoopMode 名はユニークに保つ**: + `irondash` や他ライブラリとの名前衝突を防ぐため、クレート固有のプレフィックスを維持してください。 diff --git a/docs/testing.md b/docs/testing.md index 6de3b27..03d97c3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,49 +1,49 @@ -# run_loop テストガイド +# run_loop Testing Guide -run_loop を使ったテストには、通常のテストヘルパーと GUI 用のテストハーネスの 2 種類があります。 +There are two kinds of test infrastructure for run_loop: the standard async test helper and the GUI test harness. -## test_helper(通常の非同期テスト用) +## test_helper (for ordinary async tests) -非同期コードをテストする際に使用します。RunLoop の初期化・実行・終了を自動的に処理します。 +Use this when testing async code. It automatically handles RunLoop initialization, execution, and teardown. -### 使い方 +### Usage ```rust use novonotes_run_loop::test_helper as test; use serial_test::serial; #[test] -#[serial] // 複数テストの場合、直列実行が必須。 +#[serial] // Required for serialization when running multiple tests. fn test_example() { test::run_async(async { - // ここに非同期テストコードを書く + // Write async test code here RunLoop::current().delay(Duration::from_millis(10)).await; - 42 // 任意の型を返せる + 42 // Any type can be returned }); } ``` -- **必ず `#[serial]` を付ける**: RunLoop は複数スレッドによる同時実行をサポートしません。 -- **戻り値は自由**: `Result` や任意の型を返せます -- **パニックも処理**: テスト内のパニックは適切にキャッチされ、テスト失敗として報告されます +- **Always attach `#[serial]`**: RunLoop does not support concurrent execution by multiple threads. +- **Return type is flexible**: You can return `Result` or any other type. +- **Panics are handled**: Panics inside the test are caught and reported as test failures. -## test_harness(GUI 統合テスト用) +## test_harness (for GUI integration tests) -macOS/iOS などで GUI 操作が必要なテストは、メインスレッドで実行する必要があります。この場合は標準テストハーネスを無効化して、専用のハーネスを使います。 +Tests that require GUI operations on macOS/iOS must run on the main thread. In those cases, disable the standard test harness and use the dedicated one instead. -### 設定 +### Setup -Rust の標準ハーネスはテストをメインスレッドで実行しないため、無効化する必要があります。 +The standard Rust harness does not run tests on the main thread, so it must be disabled. ```toml # Cargo.toml [[test]] name = "gui_test" path = "tests/gui_test.rs" -harness = false # 標準ハーネスを無効化 +harness = false # Disable the standard harness ``` -### 使い方 +### Usage ```rust use novonotes_run_loop::test_harness::run_gui_tests; @@ -51,20 +51,20 @@ use novonotes_run_loop::test_harness::run_gui_tests; fn main() { run_gui_tests(vec![ ("test_name", test_function), - // 複数のテストを追加可能 + // Add more tests as needed ]); } fn test_function() -> Result<(), String> { RunLoop::current().schedule(Duration::ZERO, move || { - // 何かしらの GUI テストコード + // Some GUI test code assert_eq!(1 + 1, 2); RunLoop::current().stop_app(); }) .detach(); - // run_app は stop_app が呼ばれるまで処理をブロックする。 + // run_app blocks until stop_app is called. RunLoop::current().run_app(); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 434d04e..45fb134 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,16 @@ -//! プラットフォーム固有のネイティブ run loop(CFRunLoop / ALooper / GMainContext / Win32 -//! メッセージループ)を共通の API で扱うクレートです。 -//! [irondash_run_loop](https://github.com/irondash/irondash) をベースに、 -//! DLL・オーディオプラグイン環境での安全性を強化したフォークです。 +//! A crate that provides a unified API over platform-specific native run loops +//! (CFRunLoop / ALooper / GMainContext / Win32 message loop). +//! A fork of [irondash_run_loop](https://github.com/irondash/irondash) with enhanced safety +//! for DLL and audio plugin environments. //! -//! 使い方・サンプルコードは [README](https://github.com/novonotes/run_loop) を参照してください。 -//! 設計の背景は [docs/maintainers.md](../docs/maintainers.md) を参照してください。 +//! For usage examples see the [README](https://github.com/novonotes/run_loop). +//! For design background see [docs/maintainers.md](../docs/maintainers.md). //! -//! ## 注意点 +//! ## Notes //! -//! - [`RunLoop::current()`] は run loop スレッドからのみ呼び出せます。他スレッドからは [`RunLoop::sender()`] を使ってください。 -//! - `init()` と `deinit()` は必ず対にしてください(内部で参照カウントを管理しています)。 -//! - テストは singleton 制約があるため `#[serial_test::serial]` で直列化が必要です([`test_harness`] 参照)。 +//! - [`RunLoop::current()`] may only be called from the run loop thread. Use [`RunLoop::sender()`] from other threads. +//! - Always pair `init()` with `deinit()` (the implementation uses reference counting internally). +//! - Tests have a singleton constraint and must be serialized with `#[serial_test::serial]` (see [`test_harness`]). #![allow(clippy::new_without_default)] diff --git a/src/main_thread.rs b/src/main_thread.rs index 9d3a1fd..a62a9f6 100644 --- a/src/main_thread.rs +++ b/src/main_thread.rs @@ -45,7 +45,7 @@ impl MainThreadFacilitator { } } - /// 現在のスレッドの設定をリセットする + /// Resets the facilitator for the current thread. pub(crate) fn reset() { let mut facilitator = MAIN_THREAD_FACILITATOR.lock().unwrap(); *facilitator = None; diff --git a/src/platform/android/mod.rs b/src/platform/android/mod.rs index ddf149f..6a6d5a5 100644 --- a/src/platform/android/mod.rs +++ b/src/platform/android/mod.rs @@ -40,10 +40,10 @@ struct State { type SenderCallback = Box; pub struct PollSession { - /// `RunLoop::block_on` 中のポーリング状態。 + /// Polling state for `RunLoop::block_on`. /// - /// 最初の短時間は非ブロッキングで積極的にポーリングし、 - /// 一定時間経過後は同じ looper でブロッキング待機に切り替える。 + /// For the first few milliseconds, poll non-blocking aggressively. + /// After that, switch to blocking wait on the same looper. start: Instant, timed_out: bool, } diff --git a/src/platform/darwin/mod.rs b/src/platform/darwin/mod.rs index fa92794..e933d74 100644 --- a/src/platform/darwin/mod.rs +++ b/src/platform/darwin/mod.rs @@ -58,12 +58,12 @@ impl State { let run_loop = unsafe { CFRunLoopGetCurrent() }; let run_loop: CFRunLoopRef = unsafe { CFRetain(run_loop as *mut _) } as *mut _; - // オーディオプラグインなどの複数 DLL から同時に利用される場合でも、 - // 各DLLが独自の RunLoopMode を持つように、 - // タイムスタンプを使用して一意の名前を生成する + // Generate a unique name using a timestamp so that each DLL gets its own + // RunLoopMode even when multiple DLLs (e.g. audio plugins) use this crate + // simultaneously. // - // 同じ DLL を unload/load 繰り返した場合にも、クリーンな実行環境にするため、 - // DLL の ID ではなく、タイムスタンプを使用。 + // A timestamp is used instead of the DLL's identity so that repeated + // unload/reload cycles always start with a clean execution environment. let timestamp_suffix = crate::util::get_timestamp_suffix(); let run_loop_mode = format!("IrondashRunLoopMode_{}", timestamp_suffix); @@ -292,10 +292,10 @@ impl Drop for PlatformRunLoop { } pub struct PollSession { - /// `RunLoop::block_on` 中のポーリング状態。 + /// Polling state for `RunLoop::block_on`. /// - /// 最初の短時間は低遅延のため短いタイムアウトでポーリングし、 - /// 一定時間経過後は同じカスタムモードのまま長めに待機する。 + /// For the first few milliseconds, poll with a short timeout for low latency. + /// After that, wait longer in the same custom mode to reduce CPU usage. start: Instant, timed_out: bool, } @@ -417,11 +417,11 @@ impl PlatformRunLoop { pub fn poll_once(&self, poll_session: &mut PollSession) { let run_loop_mode = self.state.lock().unwrap().run_loop_mode.clone(); if !poll_session.timed_out { - // 低遅延のため最初の 6ms は短いタイムアウトでポーリングする。 + // For the first 6ms, poll with a short timeout for low latency. unsafe { CFRunLoopRunInMode(Id::as_ptr(&run_loop_mode) as CFStringRef, 0.006, 1) }; poll_session.timed_out = poll_session.start.elapsed() >= Duration::from_millis(6); } else { - // 6ms 経過後はカスタムモードのまま長めに待機し CPU 消費を抑える。 + // After 6ms, wait longer in the same custom mode to reduce CPU usage. unsafe { CFRunLoopRunInMode(Id::as_ptr(&run_loop_mode) as CFStringRef, 1.0, 1) }; } } diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index c26f3c2..1ff2ef6 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -23,10 +23,10 @@ pub type HandleType = usize; pub const INVALID_HANDLE: HandleType = 0; pub struct PollSession { - /// `RunLoop::block_on` 中のポーリング状態。 + /// Polling state for `RunLoop::block_on`. /// - /// 最初の短時間は非ブロッキングで積極的にポーリングし、 - /// 一定時間経過後は同じコンテキストでブロッキング待機に切り替える。 + /// For the first few milliseconds, poll non-blocking aggressively. + /// After that, switch to blocking wait on the same context. start: Instant, timed_out: bool, } @@ -240,11 +240,11 @@ impl PlatformRunLoop { pub fn poll_once(&self, poll_session: &mut PollSession) { if !poll_session.timed_out { - // 最初の 6ms は非ブロッキングで積極的に poll + // For the first 6ms, poll non-blocking aggressively unsafe { g_main_context_iteration(self.context.0, GFALSE) }; poll_session.timed_out = poll_session.start.elapsed() >= Duration::from_millis(6); } else { - // その後は同じコンテキストでブロッキング待機 + // After that, switch to blocking wait on the same context unsafe { g_main_context_iteration(self.context.0, GTRUE) }; } } @@ -317,7 +317,7 @@ impl PlatformRunLoopSender { // which is not expected and may lead to deadlocks. if get_system_thread_id() == self.thread_id { assert!(unsafe { g_main_context_is_owner(self.context.0) == GTRUE }); - // 直接 g_timeout_source を作成してスケジュール(RunLoop::current() を使わない) + // Schedule directly via g_timeout_source without using RunLoop::current() unsafe { unsafe extern "C" fn sender_trampoline( func: gpointer, diff --git a/src/platform/win32/adapter.rs b/src/platform/win32/adapter.rs index 3300db2..ccab05b 100644 --- a/src/platform/win32/adapter.rs +++ b/src/platform/win32/adapter.rs @@ -20,12 +20,12 @@ impl WindowClass { } fn new() -> Self { - // オーディオプラグインなどの複数 DLL から同時に利用される場合でも、 - // 各DLLが独自のウィンドウクラス名を持つように、 - // タイムスタンプを使用して一意の名前を生成する + // Generate a unique name using a timestamp so that each DLL gets its own + // Window Class name even when multiple DLLs (e.g. audio plugins) use this crate + // simultaneously. // - // 同じ DLL を unload/load 繰り返した場合にも、クリーンな実行環境にするため、 - // DLL の ID ではなく、タイムスタンプを使用。 + // A timestamp is used instead of the DLL's identity so that repeated + // unload/reload cycles always start with a clean execution environment. let timestamp_suffix = crate::util::get_timestamp_suffix(); let class_name = format!("IrondashCoreMessageWindow_{}", timestamp_suffix); let res = WindowClass { diff --git a/src/platform/win32/mod.rs b/src/platform/win32/mod.rs index f8d6d21..fff51bf 100644 --- a/src/platform/win32/mod.rs +++ b/src/platform/win32/mod.rs @@ -105,10 +105,10 @@ struct State { } pub struct PollSession { - /// `RunLoop::block_on` 中のポーリング状態。 + /// Polling state for `RunLoop::block_on`. /// - /// 最初の短時間は非ブロッキングで積極的にポーリングし、 - /// 一定時間経過後はこの RunLoop 専用 HWND のメッセージをブロッキング待機する。 + /// For the first few milliseconds, poll non-blocking aggressively. + /// After that, block-wait on messages for this RunLoop's dedicated HWND. start: Instant, timed_out: bool, } diff --git a/src/run_loop.rs b/src/run_loop.rs index cef83a0..28a7dc8 100644 --- a/src/run_loop.rs +++ b/src/run_loop.rs @@ -23,17 +23,18 @@ use crate::{ util::FutureCompleter, }; -// ランループスレッドからのみアクセスされる静的変数用のラッパー -// Sendだが!Syncな型を静的変数に格納できるようにする +// Wrapper for static variables that are only accessed from the run loop thread. +// Allows storing Send-but-!Sync types in static variables. struct RunLoopThreadOnly { inner: std::cell::UnsafeCell>, } -// MainThreadOnlyは!Sendな型も格納できるが、 -// ランループスレッドからのみアクセスされることを前提としている +// RunLoopThreadOnly can hold !Send types, but access is only allowed from the +// run loop thread. unsafe impl Send for RunLoopThreadOnly {} unsafe impl Sync for RunLoopThreadOnly {} -// 警告: この実装は危険!ランループスレッドからのみアクセスすることを保証する必要がある +// Warning: this implementation is unsafe! The caller must guarantee that access +// is restricted to the run loop thread. impl RunLoopThreadOnly { const fn new() -> Self { @@ -69,11 +70,11 @@ impl RunLoopThreadOnly { } } -// グローバルシングルトン実装 +// Global singleton static RUN_LOOP_INSTANCE: RunLoopThreadOnly> = RunLoopThreadOnly::new(); static RUN_LOOP_THREAD_ID: Mutex> = Mutex::new(None); -// CLAPパターンに従った初期化カウント +// Initialization counter following the CLAP pattern static INIT_COUNT: AtomicUsize = AtomicUsize::new(0); static INIT_MUTEX: Mutex<()> = Mutex::new(()); static BLOCK_ON_ACTIVE: AtomicBool = AtomicBool::new(false); @@ -121,9 +122,9 @@ impl Wake for BlockOnWaker { } fn wake_by_ref(self: &Arc) { - // `block_on` は待機中に platform の poll ループへ制御を返しているため、 - // future が wake されたら RunLoop をもう一度回すきっかけが必要になる。 - // ここでは空コールバックを 1 件だけ再投入し、過剰な wake スパムを避ける。 + // `block_on` yields control back to the platform poll loop while waiting, + // so when the future is woken we need to trigger another run loop iteration. + // We re-enqueue a single empty callback to avoid excessive wake spam. if !self.queued.swap(true, Ordering::AcqRel) { self.sender.send(|| {}); } @@ -138,15 +139,15 @@ struct RunLoopInner { impl Drop for RunLoopInner { fn drop(&mut self) { - // 通常は shutdown() で has_shutdown が設定されるが、 - // 異常終了時(deinit() が呼ばれずに DLL がアンロードされる等)にも - // has_shutdown を true にすることで、他のコードが適切に動作を停止できるようにする + // Normally has_shutdown is set by shutdown(), but in abnormal exit scenarios + // (e.g. DLL unloaded without calling deinit()) we set it here so other code + // can stop cleanly. self.has_shutdown.store(true, Ordering::SeqCst); - // アクティブタスクのクリーンアップ + // Clean up active tasks if let Ok(tasks) = self.active_tasks.lock() { - // タスクは既にWeakなので、強制的なabortは不要 - // ただしログを出力 + // Tasks are already held as Weak, so no forced abort is required. + // Log a warning if any are still alive. let active_count = tasks.iter().filter(|t| t.upgrade().is_some()).count(); if active_count > 0 { warn!( @@ -156,38 +157,38 @@ impl Drop for RunLoopInner { } } - // プラットフォーム固有のクリーンアップは各プラットフォームの Drop 実装で自動的に行われる + // Platform-specific cleanup is handled automatically by each platform's Drop impl. } } -/// プラットフォーム固有の RunLoop を抽象化した型 +/// An abstraction over the platform-specific RunLoop. /// -/// ランループスレッドとは +/// ## Run loop thread /// -/// RunLoop::init() を呼び出したスレッドのこと。任意の時点で存在できるランループスレッドは -/// システム全体で単一のスレッドのみ(MUST)。このスレッドでのみ RunLoop::current() が使用可能。 +/// The thread that called `RunLoop::init()`. At any given moment only a single run loop thread +/// may exist in the entire process (MUST). `RunLoop::current()` is only usable on this thread. /// -/// - 通常のアプリケーション: メインスレッドで RunLoop::init() を呼び出すべき(SHOULD) -/// - テスト環境: 任意のスレッドをランループスレッドに指定可能(MAY) -/// - スレッド切り替え: deinit() 後に別スレッドで init() することで切り替え可能(MAY) +/// - Ordinary applications: should call `RunLoop::init()` on the main thread (SHOULD) +/// - Test environments: any thread may be designated as the run loop thread (MAY) +/// - Thread switching: can be changed by calling `deinit()` then `init()` on another thread (MAY) pub struct RunLoop { inner: Arc, } #[derive(Debug, Clone)] pub enum Error { - /// エンジンコンテキストプラグインが読み込まれていない。 - /// メインスレッドsenderへのアクセスにはirondash_engine_context Flutterプラグインが必要。 + /// The engine-context plugin is not loaded. + /// Accessing the main-thread sender requires the irondash_engine_context Flutter plugin. #[cfg(feature = "flutter")] EngineContextPluginError(irondash_engine_context::Error), - /// RunLoopは既に初期化されている + /// RunLoop is already initialized. AlreadyInitialized, - /// RunLoopが初期化されていない。先にRunLoop::init()を呼び出すこと + /// RunLoop is not initialized. Call RunLoop::init() first. NotInitialized, - /// RunLoopスレッド以外から呼び出された + /// Called from a thread that is not the run loop thread. NotRunLoopThread, #[cfg(test)] @@ -230,17 +231,15 @@ impl Display for Error { impl std::error::Error for Error {} -// thread_local削除 - グローバルシングルトンに置き換え - impl RunLoop { - /// アプリケーション/DLLの初期化時に呼び出す(CLAPのinit、VST3のInitDll相当) - /// 複数回呼ばれても安全(防御的実装) + /// Call during application/DLL initialization (equivalent to CLAP `init` or VST3 `InitDll`). + /// Safe to call multiple times (defensive implementation). pub fn init() -> Result<()> { let _guard = INIT_MUTEX.lock().unwrap(); let count = INIT_COUNT.fetch_add(1, Ordering::SeqCst); if count == 0 { - // 初回のみ実際の初期化 + // Only initialize on the first call Self::initialize()?; } @@ -251,7 +250,7 @@ impl RunLoop { Ok(()) } - /// 現在のスレッドをランループスレッドとして保証する + /// Ensures the current thread is the run loop thread. pub fn ensure_run_loop_on_current_thread() -> Result<()> { let guard = INIT_MUTEX.lock().unwrap(); let count = INIT_COUNT.load(Ordering::SeqCst); @@ -274,27 +273,27 @@ impl RunLoop { Ok(()) } - /// アプリケーション/DLLの終了時に呼び出す(CLAPのdeinit、VST3のExitDll相当) - /// init()と同じ回数だけ呼ばれる必要がある + /// Call during application/DLL teardown (equivalent to CLAP `deinit` or VST3 `ExitDll`). + /// Must be called the same number of times as `init()`. pub fn deinit() { let _guard = INIT_MUTEX.lock().unwrap(); let count = INIT_COUNT.fetch_sub(1, Ordering::SeqCst); if count == 1 { - // 最後の呼び出しで実際のクリーンアップ + // Perform actual cleanup on the last call Self::shutdown(); } } - /// 内部使用のみ:実際の初期化処理 + /// Internal only: performs the actual initialization. fn initialize() -> Result<()> { - // 現在のスレッドをランループスレッドとして記録 + // Record the current thread as the run loop thread { let mut thread_id = RUN_LOOP_THREAD_ID.lock().unwrap(); *thread_id = Some(thread::current().id()); } - // RunLoopインスタンスを作成 + // Create the RunLoop instance let inner = Arc::new(RunLoopInner { platform_run_loop: Rc::new(PlatformRunLoop::new()), active_tasks: Mutex::new(Vec::new()), @@ -305,22 +304,22 @@ impl RunLoop { .set(inner) .map_err(|_| Error::AlreadyInitialized)?; - // MainThreadFacilitatorを設定(Flutter pluginがない環境でも動作するように) + // Set up MainThreadFacilitator (works even without Flutter plugin) MainThreadFacilitator::set_for_current_thread(); Ok(()) } - /// 内部使用のみ:実際のクリーンアップ処理 + /// Internal only: performs the actual cleanup. fn shutdown() { if let Some(instance) = RUN_LOOP_INSTANCE.get() { - // シャットダウン完了を記録 + // Record that shutdown is complete instance.has_shutdown.store(true, Ordering::SeqCst); - // アクティブタスクをすべてabort - // abort 中の panic をキャッチしてクラッシュを防ぐ。 - // オーディオプラグインでは DAW をクラッシュさせないことが最優先。 - // これは保険であり、本来は panic が起きない設計にすべき。 + // Abort all active tasks. + // Catch any panics during abort to prevent crashes. + // In audio plugins, not crashing the DAW host is the top priority. + // This is a safety net; ideally no panic should occur here. if let Ok(tasks) = instance.active_tasks.lock() { for weak_task in tasks.iter() { if let Some(task) = weak_task.upgrade() { @@ -338,34 +337,34 @@ impl RunLoop { } } - // アクティブタスクのリストをクリア + // Clear the active task list if let Ok(mut tasks) = instance.active_tasks.lock() { tasks.clear(); } - // プラットフォーム固有のクリーンアップは各プラットフォームの Drop 実装で自動的に行われる + // Platform-specific cleanup is handled automatically by each platform's Drop impl. } - // ランループスレッドIDをクリア(次のinit()で新しいスレッドを設定可能にする) + // Clear the run loop thread ID so a new thread can be set by the next init() { let mut thread_id = RUN_LOOP_THREAD_ID.lock().unwrap(); *thread_id = None; } - // RunLoopインスタンスもクリア(次のinit()で新しいインスタンスを作成可能にする) + // Clear the RunLoop instance so a new one can be created by the next init() RUN_LOOP_INSTANCE.clear(); - // MainThreadFacilitatorをリセット + // Reset MainThreadFacilitator MainThreadFacilitator::reset(); } - /// 指定された遅延後にコールバックを実行するようスケジュール。 + /// Schedules `callback` to be executed after `in_time`. /// - /// コールバックが実行されるまで保持する必要がある[`Handle`]を返す。 - /// ハンドルが早期にドロップされると、コールバックはキャンセルされる。 + /// Returns a [`Handle`] that must be kept alive until the callback executes. + /// Dropping the handle early cancels the callback. /// - /// * [`Handle::detach()`]を呼ぶとハンドルをドロップしても実行が保証される - /// * [`Handle::cancel()`]でハンドルをドロップせずにキャンセル可能 + /// * Call [`Handle::detach()`] to ensure execution even after the handle is dropped. + /// * Call [`Handle::cancel()`] to cancel without dropping the handle. #[must_use] pub fn schedule(&self, in_time: Duration, callback: F) -> Handle where @@ -379,7 +378,7 @@ impl RunLoop { }) } - /// 指定された時間後に完了するFutureを返す。 + /// Returns a Future that completes after the specified duration. pub async fn delay(&self, duration: Duration) { let (future, completer) = FutureCompleter::<()>::new(); self.schedule(duration, move || { @@ -389,7 +388,7 @@ impl RunLoop { future.await } - /// ランループスレッドへコールバックを送信できるsenderオブジェクトを返す。 + /// Returns a sender object that can post callbacks to the run loop thread. pub fn sender() -> RunLoopSender { if Self::is_run_loop_thread() { RunLoop::current().new_sender() @@ -400,14 +399,13 @@ impl RunLoop { } } - /// 他のスレッドからこのランループでコールバックを実行するための - /// senderオブジェクトを返す。 - /// senderは`RunLoop`と異なり`Send`と`Sync`を実装している。 + /// Returns a sender object that allows other threads to execute callbacks on this run loop. + /// Unlike `RunLoop`, the sender implements `Send` and `Sync`. pub(crate) fn new_sender(&self) -> RunLoopSender { RunLoopSender::new(self.inner.platform_run_loop.new_sender()) } - /// 現在のスレッドがランループスレッドかどうかを返す + /// Returns whether the current thread is the run loop thread. pub fn is_run_loop_thread() -> bool { let thread_id = RUN_LOOP_THREAD_ID.lock().unwrap(); if let Some(run_loop_thread_id) = *thread_id { @@ -418,15 +416,15 @@ impl RunLoop { } } - /// 現在のスレッドをランループスレッドとして設定 + /// Sets the current thread as the run loop thread. /// /// [deprecated] - /// もしかしたら古い Flutter と統合する際に必要かもしれないので残してあるだけ。 - /// 基本的にこのメソッドではなく、RunLoop::init() によって RunLoopスレッドを指定するべき。 + /// Retained only in case it is needed for integrating with older Flutter versions. + /// In general, the run loop thread should be designated via `RunLoop::init()` instead. /// - /// irondash_engine_contextプラグインを使用しない場合、 - /// RunLoop::init()の後にランループスレッドで呼び出す。 - /// これによりFlutterプラグインなしでRunLoop::sender_for_run_loop_thread()が動作する。 + /// Call from the run loop thread after `RunLoop::init()` when not using the + /// irondash_engine_context plugin. This allows `RunLoop::sender_for_run_loop_thread()` + /// to work without a Flutter plugin. #[deprecated(note = "Use RunLoop::init() instead")] pub fn set_run_loop_thread() { { @@ -434,26 +432,26 @@ impl RunLoop { *thread_id = Some(thread::current().id()); } - // MainThreadFacilitatorを設定(Flutter pluginがない環境でも動作するように) + // Set up MainThreadFacilitator (works even without Flutter plugin) use crate::main_thread::MainThreadFacilitator; MainThreadFacilitator::set_for_current_thread(); } - /// このランループをエグゼキュータとしてFutureをスポーンする。 + /// Spawns a Future using this run loop as the executor. pub fn spawn(&self, future: impl Future + 'static) -> JoinHandle { - // シャットダウンチェック + // Check for shutdown if self.inner.has_shutdown.load(Ordering::SeqCst) { panic!("Cannot spawn task on shut down RunLoop"); } let task = Arc::new(Task::new(self.new_sender(), future)); - // タスクを追跡リストに追加 + // Track the task { let mut tasks = self.inner.active_tasks.lock().unwrap(); tasks.push(Arc::downgrade(&(task.clone() as Arc))); - // デッドタスクを定期的にクリーンアップ + // Periodically clean up dead tasks if tasks.len() > 100 { tasks.retain(|weak| weak.upgrade().is_some()); } @@ -463,29 +461,30 @@ impl RunLoop { JoinHandle::new(task) } - /// 指定されたFutureが完了するまで現在のスレッドを同期的に待機する。 + /// Synchronously blocks the current thread until the given Future completes. /// - /// このメソッドは待機中も RunLoop を駆動し続けるため、`spawn` で投入された他タスクも実行される。 - /// そのため、待機対象 Future が RunLoop 上の別タスク完了に依存していても進行できる。 - /// 一方で `pollster::block_on` など外部 executor は RunLoop を駆動しないため、 - /// 同じ依存関係ではデッドロックしうる。 + /// This method continues to drive the RunLoop while waiting, so other tasks submitted + /// via `spawn` can also make progress. This means a Future that depends on another task + /// completing on the run loop will not deadlock. In contrast, external executors such as + /// `pollster::block_on` do not drive the run loop and would deadlock in the same situation. /// - /// `spawn` は使わず、呼び出し元の future をその場で直接 poll する。 - /// そのため `RunLoop::spawn` と異なり、non-`'static` な借用を含む future も扱える。 + /// Unlike `spawn`, this method polls the provided future directly in place without spawning it. + /// Therefore, unlike `RunLoop::spawn`, it can accept futures that contain non-`'static` borrows. /// - /// `flutter` feature 有効時、通常のポーリング (`run` 用) は必要に応じてプラットフォーム既定の - /// イベントキューへフォールバックする場合があるが、`block_on` では常に RunLoop 固有ソースのみを処理する。 + /// When the `flutter` feature is enabled, the normal polling path (for `run`) may fall back to + /// the platform default event queue as needed, but `block_on` always processes only RunLoop + /// specific sources. /// - /// ネストした `block_on` はパニックする。 + /// Nested `block_on` calls will panic. /// - /// # 使用例 + /// # Example /// /// ```no_run /// use novonotes_run_loop::RunLoop; /// /// RunLoop::init().unwrap(); /// let result = RunLoop::current().block_on(async { - /// // 非同期処理 + /// // Async work /// 42 /// }); /// assert_eq!(result, 42); @@ -505,8 +504,8 @@ impl RunLoop { let mut context = Context::from_waker(&waker); let mut future = pin!(future); - // stop() は使わず、待機対象 future が wake されるたびに再 poll しつつ - // RunLoop 固有ソースを回し続ける。 + // Rather than using stop(), re-poll the target future each time it is woken + // while continuing to drive RunLoop-specific sources. let mut poll_session = PollSession::new(); loop { @@ -522,11 +521,10 @@ impl RunLoop { } } - /// 現在のスレッドのRunLoopを返す - /// 必ずランループスレッドから呼び出すべき。 - /// それ以外のスレッドの場合はパニックする。 + /// Returns the RunLoop for the current thread. + /// Must be called from the run loop thread; panics otherwise. pub fn current() -> Self { - // ランループスレッドチェック + // Verify we are on the run loop thread let current_thread = thread::current().id(); let thread_id = RUN_LOOP_THREAD_ID.lock().unwrap(); @@ -538,12 +536,12 @@ impl RunLoop { panic!("RunLoop not initialized. Call RunLoop::init() first"); } - // インスタンス取得 + // Retrieve the instance let instance = RUN_LOOP_INSTANCE .get() .expect("RunLoop not initialized. Call RunLoop::init() first"); - // シャットダウンチェック + // Check for shutdown if instance.has_shutdown.load(Ordering::SeqCst) { panic!("RunLoop has been shut down"); } @@ -553,11 +551,11 @@ impl RunLoop { } } - /// 現在のスレッドのRunLoopを取得する失敗可能なメソッド。 + /// Fallible variant of [`RunLoop::current()`]. /// - /// 現在のスレッドで RunLoop が初期化されていない場合はエラーを返す。 + /// Returns an error if the RunLoop is not initialized on the current thread. pub fn try_current() -> Result { - // ランループスレッドチェック + // Verify we are on the run loop thread let current_thread = thread::current().id(); let thread_id = RUN_LOOP_THREAD_ID.lock().unwrap(); @@ -569,10 +567,10 @@ impl RunLoop { return Err(Error::NotInitialized); } - // インスタンス取得 + // Retrieve the instance let instance = RUN_LOOP_INSTANCE.get().ok_or(Error::NotInitialized)?; - // シャットダウンチェック + // Check for shutdown if instance.has_shutdown.load(Ordering::SeqCst) { return Err(Error::NotInitialized); } @@ -582,17 +580,17 @@ impl RunLoop { }) } - /// 停止されるまでランループを実行する。 + /// Runs the run loop until stopped. /// - /// スタンドアロンアプリなど、自前でランループを駆動する場合に使用する。 - /// プラグイン環境ではホストが既にループを回しているため、通常は呼び出す必要がない。 + /// Use this in standalone applications that drive their own run loop. + /// In plugin environments the host already drives the loop, so this is normally not needed. /// - /// 呼び出し前に `RunLoop::init()` が完了している必要がある。 + /// `RunLoop::init()` must have completed before calling this. pub fn run(&self) { self.inner.platform_run_loop.run() } - /// ランループを停止する。 + /// Stops the run loop. pub fn stop(&self) { self.inner.platform_run_loop.stop() } @@ -608,8 +606,8 @@ impl RunLoop { } } -/// 現在のスレッドのランループをエグゼキュータとしてFutureをスポーンする。 -/// 事前にRunLoop::init()で初期化されている必要がある。 +/// Spawns a Future using the current thread's RunLoop as the executor. +/// The RunLoop must have been initialized with `RunLoop::init()` beforehand. pub fn spawn(future: impl Future + 'static) -> JoinHandle { RunLoop::current().spawn(future) } @@ -657,7 +655,7 @@ mod tests { let sender = run_loop.new_sender(); let stop_called = Arc::new(Mutex::new(false)); let stop_called_clone = stop_called.clone(); - // ランループが既に実行中のときにスレッドをスポーンすることを確認 + // Confirm that a thread can be spawned while the run loop is already running // run_loop.schedule(Duration::from_secs(1000), || {}).detach(); run_loop .schedule(Duration::from_secs(0), || { @@ -691,7 +689,7 @@ mod tests { }); }); - // コールバックが実行されるまで待機 + // Wait until the callback executes rx.await.unwrap(); handle.join().unwrap(); @@ -717,14 +715,14 @@ mod tests { #[test] #[serial] fn test_init_deinit_reinit() { - // 初回のinit + // First init RunLoop::init().unwrap(); assert!(RunLoop::is_run_loop_thread()); - // deinitで状態をクリア + // deinit clears the state RunLoop::deinit(); - // 別のスレッドで再度init可能 + // Can re-init on another thread let handle = thread::spawn(|| { RunLoop::init().unwrap(); assert!(RunLoop::is_run_loop_thread()); @@ -732,7 +730,7 @@ mod tests { }); handle.join().unwrap(); - // 元のスレッドでも再度init可能 + // Can re-init on the original thread as well RunLoop::init().unwrap(); assert!(RunLoop::is_run_loop_thread()); RunLoop::deinit(); @@ -743,20 +741,18 @@ mod tests { fn test_deinit_aborts_all_tasks() { use std::sync::atomic::{AtomicBool, Ordering}; - // 初期化 RunLoop::init().unwrap(); - // タスクが実行されたかを追跡 + // Track whether each task has started let task1_started = Arc::new(AtomicBool::new(false)); let task2_started = Arc::new(AtomicBool::new(false)); let t1_started = task1_started.clone(); let t2_started = task2_started.clone(); - // 長時間実行されるタスクをスポーン + // Spawn long-running tasks that wait to be aborted let handle1 = RunLoop::current().spawn(async move { t1_started.store(true, Ordering::SeqCst); - // 無限ループでabort待ち loop { std::future::pending::<()>().await; } @@ -764,33 +760,30 @@ mod tests { let handle2 = RunLoop::current().spawn(async move { t2_started.store(true, Ordering::SeqCst); - // 無限ループでabort待ち loop { std::future::pending::<()>().await; } }); - // RunLoopを少し実行してタスクを開始させる + // Run the loop briefly to let the tasks start, then stop RunLoop::current() .schedule(Duration::from_millis(300), || { - // タスクが開始されるのを待ってから停止 RunLoop::current().stop(); }) .detach(); RunLoop::current().run(); - // タスクが開始されていたことを確認 + // Confirm the tasks started assert!(task1_started.load(Ordering::SeqCst)); assert!(task2_started.load(Ordering::SeqCst)); - // deinit()を呼ぶ - 全てのタスクがabortされるはず + // deinit() should abort all tasks RunLoop::deinit(); - // pollster::block_on でタスクがアボートされたことを確認 + // Confirm both tasks were aborted let result1 = pollster::block_on(handle1); let result2 = pollster::block_on(handle2); - // 両方のタスクがAbortedエラーを返すはず assert!(matches!(result1, Err(crate::JoinError::Aborted))); assert!(matches!(result2, Err(crate::JoinError::Aborted))); } @@ -823,7 +816,8 @@ mod tests { fn test_block_on_drives_spawned_tasks() { RunLoop::init().unwrap(); - // `block_on` 待機中も RunLoop が回り続け、別途 spawn されたタスクが進行できることを確認。 + // Verify that the RunLoop continues to run during block_on so that + // separately spawned tasks can make progress. let handle = RunLoop::current().spawn(async move { RunLoop::current().delay(Duration::from_millis(20)).await; 123 @@ -838,10 +832,10 @@ mod tests { #[test] #[serial] fn test_block_on_nested() { - // 前のテストの影響を排除するため ensure を使用 + // Use ensure to avoid interference from a previous test RunLoop::ensure_run_loop_on_current_thread().unwrap(); - // ネストした block_on は未定義動作として debug_assert で検出される + // Nested block_on is undefined behavior and should be detected let result = std::panic::catch_unwind(|| { RunLoop::current().block_on(async { let inner_result = RunLoop::current().block_on(async { "inner" }); @@ -856,10 +850,10 @@ mod tests { #[test] #[serial] fn test_block_on_panic() { - // 前のテストの影響を排除するため ensure を使用 + // Use ensure to avoid interference from a previous test RunLoop::ensure_run_loop_on_current_thread().unwrap(); - // catch_unwind でパニックをキャッチして deinit を確実に呼ぶ + // Catch the panic so deinit is always called let result = std::panic::catch_unwind(|| { RunLoop::current().block_on(async { panic!("Task panicked"); @@ -875,7 +869,8 @@ mod tests { fn test_block_on_recovers_after_panic() { RunLoop::ensure_run_loop_on_current_thread().unwrap(); - // panic で `block_on` を抜けた後もネスト判定フラグが解除され、次回呼び出しが正常動作することを確認。 + // After exiting block_on via a panic, the nesting flag should be cleared + // so subsequent calls work correctly. let panic_result = std::panic::catch_unwind(|| { RunLoop::current().block_on(async { panic!("Task panicked"); @@ -906,7 +901,7 @@ mod tests { } } - // `&mut self` を借用した non-`'static` future を `block_on` に渡せることを確認。 + // Confirm that a non-`'static` future borrowing `&mut self` can be passed to block_on. let mut counter = Counter { value: 41 }; let result = RunLoop::current().block_on(counter.increment_and_get()); diff --git a/src/task.rs b/src/task.rs index ea67050..a8a0cbf 100644 --- a/src/task.rs +++ b/src/task.rs @@ -17,28 +17,28 @@ use futures::{ use crate::RunLoopSender; -// タスクのabort機能を抽象化するトレイト +// Trait that abstracts the abort capability of a task pub(crate) trait AbortableTask: Send + Sync { - #[allow(dead_code)] // Drop実装でのみ使用 + #[allow(dead_code)] // only used in Drop impl fn abort(&self); } -/// Task 実行中に発生するエラー +/// Errors that can occur while a Task is running. #[derive(Debug)] pub enum JoinError { - /// タスクが abort() で中断された + /// The task was cancelled via `abort()`. Aborted, - /// タスク内で panic が発生した + /// A panic occurred inside the task. Panic(Box), } impl JoinError { - /// エラーがキャンセルによるものかチェック + /// Returns `true` if the error was caused by a cancellation. pub fn is_aborted(&self) -> bool { matches!(self, Self::Aborted) } - /// エラーが panic によるものかチェック + /// Returns `true` if the error was caused by a panic. pub fn is_panic(&self) -> bool { matches!(self, Self::Panic(_)) } @@ -84,7 +84,7 @@ impl Task { let future_opt = &mut *self.future.get(); match future_opt { Some(future) => { - // panic をキャッチしてエラーとして扱う + // Catch panics and treat them as an error match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(context))) { Ok(Poll::Ready(value)) => Poll::Ready(Ok(value)), Ok(Poll::Pending) => Poll::Pending, @@ -98,7 +98,7 @@ impl Task { pub(crate) fn abort(&self) { self.aborted.store(true, Ordering::Release); - // Future を drop してリソースを解放 + // Drop the Future to release resources unsafe { let future_opt = &mut *self.future.get(); *future_opt = None; diff --git a/src/test_harness.rs b/src/test_harness.rs index 9343cda..9b73ac1 100644 --- a/src/test_harness.rs +++ b/src/test_harness.rs @@ -1,10 +1,10 @@ -// カスタムテストハーネス - GUI統合テスト用のヘルパー +// Custom test harness — helper for GUI integration tests use crate::RunLoop; use log::{error, info}; -/// 複数のGUIテストを順次実行するハーネス -/// GUI 関連の操作には、必ず main スレッドで実行しなければいけない処理があります。 -/// そのような処理のテストは標準のテストハーネスでは難しいため、このハーネスを使ってください。 +/// Harness that runs multiple GUI tests sequentially. +/// Some GUI-related operations must run on the main thread and are not +/// straightforward to test with the standard harness. Use this harness for those cases. /// /// # Example /// ```ignore @@ -13,9 +13,9 @@ use log::{error, info}; /// ("test2", test_function2), /// ]); /// ``` -/// このハーネスを利用する場合、標準ハーネスを無効化する必要があります。 +/// When using this harness, the standard harness must be disabled. /// -/// Cargo.toml の例 +/// Example Cargo.toml entry: /// ```ignore /// [[test]] /// name = "wxp_webview_test" @@ -29,7 +29,7 @@ where { info!("Running GUI tests on main thread..."); - // RunLoopを初期化 + // Initialize RunLoop match RunLoop::init() { Ok(_) => {} Err(e) => { @@ -52,7 +52,7 @@ where } } - // RunLoopをクリーンアップ + // Clean up RunLoop RunLoop::deinit(); if failed { diff --git a/src/test_helper.rs b/src/test_helper.rs index d682fb4..14cb4bc 100644 --- a/src/test_helper.rs +++ b/src/test_helper.rs @@ -1,18 +1,19 @@ use crate::RunLoop; use std::sync::{Arc, Mutex}; -/// RunLoopベースの非同期テストを実行するためのヘルパー関数 -/// RunLoop に依存するテストは、serial_test の #[serial] などで、直列実行する必要があります。 -/// RunLoop は同時に複数のスレッドを RunLoop スレッドとして設定することをサポートしていないためです。 +/// Helper function for running RunLoop-based async tests. +/// Tests that depend on RunLoop must be serialized (e.g., with `#[serial]` from serial_test) +/// because RunLoop does not support designating multiple threads as the run loop thread +/// simultaneously. /// -/// # 使用例 +/// # Example /// /// ```ignore /// #[test] /// #[serial] /// fn test_something() { /// run_loop::test_helper::run_async(async { -/// // run loop 上で非同期処理を実行 +/// // Run async work on the run loop /// Ok::<(), String>(()) /// }); /// } @@ -27,10 +28,10 @@ where let result = Arc::new(Mutex::new(None)); let result_clone = result.clone(); - // 非同期関数を実行 + // Spawn the async function let handle = run_loop.spawn(test_fn); - // 完了を待ってRunLoopを停止 + // Wait for completion, then stop the RunLoop run_loop.spawn(async move { match handle.await { Ok(test_result) => { @@ -52,7 +53,7 @@ where RunLoop::deinit(); - // 結果を取り出して返す + // Extract and return the result let result = Arc::try_unwrap(result) .map_err(|_| "Failed to unwrap Arc") .unwrap() diff --git a/src/util/mod.rs b/src/util/mod.rs index b29911c..c6945ad 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,7 +8,7 @@ pub use future_completer::*; use std::time::{SystemTime, UNIX_EPOCH}; -/// ミリ秒精度のタイムスタンプを大文字英数字で圧縮した文字列を返す +/// Returns the current millisecond timestamp encoded as a base-36 uppercase string. pub fn get_timestamp_suffix() -> String { const BASE36_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -41,19 +41,19 @@ mod tests { #[test] fn test_get_timestamp_suffix() { - // 空でない文字列を返すこと + // Should return a non-empty string let suffix = get_timestamp_suffix(); debug!("suffix: {}", suffix); assert!(!suffix.is_empty()); - // 大文字英数字のみで構成されていること + // Should consist only of uppercase alphanumeric characters assert!( suffix.chars().all( |c| c.is_ascii_alphanumeric() && (c.is_ascii_digit() || c.is_ascii_uppercase()) ) ); - // 連続して呼び出すと異なる値を返すこと(ミリ秒精度なので sleep を入れる) + // Consecutive calls should return different values (millisecond precision, so sleep) let suffix1 = get_timestamp_suffix(); std::thread::sleep(std::time::Duration::from_millis(2)); let suffix2 = get_timestamp_suffix(); diff --git a/tests/helper_test.rs b/tests/helper_test.rs index d2507e1..329d038 100644 --- a/tests/helper_test.rs +++ b/tests/helper_test.rs @@ -2,12 +2,12 @@ use novonotes_run_loop::{RunLoop, test_helper as test}; use serial_test::serial; use std::time::Duration; -// ヘルパー関数を使ったテスト例 +// Example tests using the helper function #[test] #[serial] fn test_success() -> Result<(), String> { let result = test::run_async(async { - // 成功するテスト + // A test that succeeds Ok(()) }); result @@ -17,7 +17,7 @@ fn test_success() -> Result<(), String> { #[serial] fn test_async_wait() -> Result<(), String> { let result = test::run_async(async { - // RunLoop の wait 機能を使ったテスト + // Test using RunLoop's delay RunLoop::current().delay(Duration::from_millis(10)).await; Ok(()) }); @@ -28,13 +28,13 @@ fn test_async_wait() -> Result<(), String> { #[serial] fn test_error_propagation() { let result: Result<(), String> = test::run_async(async { - // エラーが正しく伝播されることをテスト + // Verify that errors propagate correctly Err("Expected error".to_string()) }); assert!(result.is_err()); } -// このテストは意図的に失敗するため、ignore している。 +// This test intentionally fails and is therefore ignored. #[ignore] #[test] #[serial] diff --git a/tests/task_test.rs b/tests/task_test.rs index 651dc20..8465ac3 100644 --- a/tests/task_test.rs +++ b/tests/task_test.rs @@ -11,12 +11,12 @@ fn test_task_normal_completion() { let result = Arc::new(Mutex::new(None)); let result_clone = result.clone(); - // spawn 関数を使ってタスクを起動 + // Launch a task using the spawn function spawn(async move { *result_clone.lock().unwrap() = Some(42); }); - // RunLoop を実行してタスクを処理 + // Run the RunLoop to process the task let run_loop = RunLoop::current(); let mut handle = run_loop.schedule(Duration::from_millis(50), move || { RunLoop::current().stop(); @@ -25,7 +25,7 @@ fn test_task_normal_completion() { run_loop.run(); - // 結果を確認 + // Verify the result let res = result.lock().unwrap().take(); assert_eq!(res, Some(42)); @@ -42,15 +42,15 @@ fn test_task_abort() { let run_loop = RunLoop::current(); let handle = run_loop.spawn(async { - // 長時間実行されるタスク + // Long-running task RunLoop::current().delay(Duration::from_secs(10)).await; 42 }); - // 即座に abort + // Abort immediately handle.abort(); - // abort されたことを確認 + // Confirm the task was aborted let mut handle = Box::pin(handle); let waker = futures::task::noop_waker(); let mut cx = Context::from_waker(&waker); @@ -60,7 +60,7 @@ fn test_task_abort() { assert!(result.is_err()); assert!(result.unwrap_err().is_aborted()); } - Poll::Pending => panic!("abort されたタスクは即座に完了すべきです"), + Poll::Pending => panic!("An aborted task should complete immediately"), } RunLoop::deinit(); @@ -77,10 +77,10 @@ fn test_task_panic() { let captured_panic_clone = captured_panic.clone(); let handle = run_loop.spawn(async { - panic!("タスク内でパニック"); + panic!("panic inside task"); }); - // spawn で別のタスクを起動して panic を検証 + // Spawn another task to verify the panic result run_loop.spawn(async move { let result = handle.await; *captured_panic_clone.lock().unwrap() = Some(result); @@ -89,16 +89,16 @@ fn test_task_panic() { run_loop.run(); - // 結果を確認 + // Verify the result let result = captured_panic.lock().unwrap().take().unwrap(); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.is_panic()); - // panic メッセージを確認 + // Verify the panic message if let JoinError::Panic(payload) = err { if let Some(msg) = payload.downcast_ref::<&str>() { - assert_eq!(*msg, "タスク内でパニック"); + assert_eq!(*msg, "panic inside task"); } } @@ -114,22 +114,22 @@ fn test_multiple_tasks_mixed_results() { let run_loop = RunLoop::current(); let results = Arc::new(Mutex::new(vec![])); - // 正常完了するタスク + // Task that completes normally let handle1 = run_loop.spawn(async { 1 }); - // abort されるタスク + // Task that gets aborted let handle2 = run_loop.spawn(async { RunLoop::current().delay(Duration::from_secs(10)).await; 2 }); handle2.abort(); - // panic するタスク + // Task that panics let handle3 = run_loop.spawn(async { - panic!("意図的なパニック"); + panic!("intentional panic"); }); - // 結果を収集するタスク + // Task that collects all results let results_clone = results.clone(); run_loop.spawn(async move { let r1 = handle1.await; @@ -146,19 +146,19 @@ fn test_multiple_tasks_mixed_results() { run_loop.run(); - // 結果を確認 + // Verify all results let res = results.lock().unwrap(); assert_eq!(res.len(), 3); - // handle1: 正常完了 + // handle1: completed normally assert!(res[0].1.is_ok()); assert_eq!(res[0].1.as_ref().unwrap(), &1); - // handle2: キャンセル + // handle2: cancelled assert!(res[1].1.is_err()); assert!(res[1].1.as_ref().unwrap_err().is_aborted()); - // handle3: パニック + // handle3: panicked assert!(res[2].1.is_err()); assert!(res[2].1.as_ref().unwrap_err().is_panic());