From 33e03649cb73535785bf8489c8c34eb39d6a6c33 Mon Sep 17 00:00:00 2001 From: Jingyu Ma Date: Thu, 26 Mar 2026 22:05:47 -0700 Subject: [PATCH 1/2] chore: bump version to 0.5.1 and update documentation - Bump version to 0.5.1 in Cargo.toml, injectorpp-macros/Cargo.toml, README.md - Add CHANGELOG entry for 0.5.1 with new_global() and bug fix for #121 - Add 'Thread-local vs Global mode' section to README with usage guide and rayon example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 9 ++++++ Cargo.toml | 2 +- README.md | 59 +++++++++++++++++++++++++++++++++++- injectorpp-macros/Cargo.toml | 2 +- tests/global.rs | 2 +- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2f89f..9b9bb7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 0.5.1 (March 27, 2026) + +- Add `InjectorPP::new_global()` constructor for cross-thread fake visibility. + - Fakes registered through `new_global()` use direct code patching (0.4.0-style) and are visible to **all threads**, including background threads, timers, and thread pools like rayon. + - `new()` remains the default with thread-local dispatch for parallel test execution. + - `new_global()` acquires an exclusive lock, serializing tests that use it. +- Fix fakes not visible from rayon worker threads and background timer threads (issue #121). +- Add IAT thunk resolution to PatchGuard path on Windows x86_64. + # 0.5.0 (March 15, 2026) - Add macOS support. diff --git a/Cargo.toml b/Cargo.toml index f5c69bb..161fb4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "injectorpp" -version = "0.5.0" +version = "0.5.1" authors = ["Jingyu Ma "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index c892413..3b5a7d1 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Add `injectorpp` to the `Cargo.toml`: ```toml [dev-dependencies] -injectorpp = "0.5.0" +injectorpp = "0.5.1" ``` Below `profile.test` config is recommended to make sure `injectorpp` working correctly in tests. If you have workspace, make sure add this on the top level of `Cargo.toml`: @@ -88,6 +88,63 @@ use injectorpp::interface::injector::*; Below are multiple ways to config the function behavior. +## Thread-local vs Global mode + +By default, `InjectorPP::new()` uses **thread-local dispatch** — fakes are only visible on the thread that created the injector. This enables parallel test execution without interference between tests. + +However, if your code under test spawns background threads, timers, or uses thread pools (e.g., rayon), those worker threads won't see thread-local fakes. For these cases, use `InjectorPP::new_global()`: + +```rust +// Thread-local mode (default) — fakes only visible on the current thread +let mut injector = InjectorPP::new(); + +// Global mode — fakes visible to ALL threads (background threads, rayon, timers) +let mut injector = InjectorPP::new_global(); +``` + +`new_global()` uses direct code patching, making fakes visible process-wide. It acquires an exclusive lock, so tests using `new_global()` run serialized to prevent interference. + +**When to use `new_global()`:** +- Your faked function is called from a background thread or timer +- You use `rayon::join`, `rayon::par_iter`, or similar thread pool APIs +- You need the same behavior as injectorpp 0.4.0 + +**Example with rayon:** + +```rust +fn some_string() -> String { + "original".into() +} + +fn some_other_string() -> String { + "original".into() +} + +#[test] +fn test_rayon_join_with_global_fakes() { + let mut injector = InjectorPP::new_global(); + + injector + .when_called(injectorpp::func!(fn (some_string)() -> String)) + .will_execute(injectorpp::fake!( + func_type: fn() -> String, + returns: "faked".into() + )); + + injector + .when_called(injectorpp::func!(fn (some_other_string)() -> String)) + .will_execute(injectorpp::fake!( + func_type: fn() -> String, + returns: "faked".into() + )); + + // Both functions run on rayon worker threads — global fakes are visible + let (a, b) = rayon::join(some_string, some_other_string); + assert_eq!(a, "faked"); + assert_eq!(b, "faked"); +} +``` + ## `will_return_boolean` If the function only returns boolean and you only want to make it constantly returns a specific boolean value, you can use `will_return_boolean`: diff --git a/injectorpp-macros/Cargo.toml b/injectorpp-macros/Cargo.toml index 0caaa33..5ec6ac2 100644 --- a/injectorpp-macros/Cargo.toml +++ b/injectorpp-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "injectorpp-macros" -version = "0.5.0" +version = "0.5.1" authors = ["Jingyu Ma "] license = "MIT" repository = "https://github.com/microsoft/injectorppforrust" diff --git a/tests/global.rs b/tests/global.rs index 0d1adba..efe9911 100644 --- a/tests/global.rs +++ b/tests/global.rs @@ -186,7 +186,7 @@ fn test_global_fake_unchecked_cross_thread() { } /// Verifies that `new()` (thread-local mode) still works correctly — fakes are NOT visible -/// from spawned threads (default 0.5.0 behavior). +/// from spawned threads (default thread-local behavior). #[test] #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "arm"))] fn test_thread_local_mode_not_visible_from_spawned_thread() { From 270a10af98bfd64d31925f799fe847274e0217ad Mon Sep 17 00:00:00 2001 From: Jingyu Ma Date: Thu, 26 Mar 2026 22:10:47 -0700 Subject: [PATCH 2/2] docs: remove rayon-specific example from README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 42 +++--------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3b5a7d1..5618acc 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,13 @@ Below are multiple ways to config the function behavior. By default, `InjectorPP::new()` uses **thread-local dispatch** — fakes are only visible on the thread that created the injector. This enables parallel test execution without interference between tests. -However, if your code under test spawns background threads, timers, or uses thread pools (e.g., rayon), those worker threads won't see thread-local fakes. For these cases, use `InjectorPP::new_global()`: +However, if your code under test spawns background threads, timers, or uses thread pools, those worker threads won't see thread-local fakes. For these cases, use `InjectorPP::new_global()`: ```rust // Thread-local mode (default) — fakes only visible on the current thread let mut injector = InjectorPP::new(); -// Global mode — fakes visible to ALL threads (background threads, rayon, timers) +// Global mode — fakes visible to ALL threads (background threads, timers, thread pools) let mut injector = InjectorPP::new_global(); ``` @@ -106,45 +106,9 @@ let mut injector = InjectorPP::new_global(); **When to use `new_global()`:** - Your faked function is called from a background thread or timer -- You use `rayon::join`, `rayon::par_iter`, or similar thread pool APIs +- You use thread pool APIs that execute work on worker threads - You need the same behavior as injectorpp 0.4.0 -**Example with rayon:** - -```rust -fn some_string() -> String { - "original".into() -} - -fn some_other_string() -> String { - "original".into() -} - -#[test] -fn test_rayon_join_with_global_fakes() { - let mut injector = InjectorPP::new_global(); - - injector - .when_called(injectorpp::func!(fn (some_string)() -> String)) - .will_execute(injectorpp::fake!( - func_type: fn() -> String, - returns: "faked".into() - )); - - injector - .when_called(injectorpp::func!(fn (some_other_string)() -> String)) - .will_execute(injectorpp::fake!( - func_type: fn() -> String, - returns: "faked".into() - )); - - // Both functions run on rayon worker threads — global fakes are visible - let (a, b) = rayon::join(some_string, some_other_string); - assert_eq!(a, "faked"); - assert_eq!(b, "faked"); -} -``` - ## `will_return_boolean` If the function only returns boolean and you only want to make it constantly returns a specific boolean value, you can use `will_return_boolean`: