shimforge/lib.rs
1// The README examples are `#[test]` functions run by tests/readme.rs, not doctests.
2#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
3#![allow(clippy::test_attr_in_doctest)]
4#![deny(missing_docs)]
5
6#[cfg(not(all(
7 any(target_arch = "x86_64", target_arch = "aarch64"),
8 any(target_os = "windows", target_os = "linux", target_os = "macos")
9)))]
10compile_error!("shimforge supports Windows, Linux, and macOS on x86-64 and ARM64");
11
12mod asynchronous;
13#[cfg(any(target_os = "linux", target_os = "macos"))]
14mod cache;
15mod code;
16mod error;
17mod executable;
18mod expectation;
19mod memory;
20mod routing;
21
22#[cfg(test)]
23mod tests;
24
25pub use asynchronous::{AsyncExpectation, AsyncMock};
26pub use error::Error;
27pub use expectation::{CallCount, Expectation, Sequence};
28
29#[doc(hidden)]
30pub use shimforge_macros::{__check_signature, __mock, __replace_local};
31
32#[doc(hidden)]
33pub mod __private {
34 pub use crate::error::check;
35 pub use crate::expectation::{CallGuard, Config, Control, Meta, Rule, State, lock};
36 pub use crate::routing::route;
37}
38
39use std::cell::Cell;
40use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError};
41use std::thread::ThreadId;
42
43static SESSION: RwLock<()> = RwLock::new(());
44thread_local! { static SESSION_ACTIVE: Cell<bool> = const { Cell::new(false) }; }
45
46enum SessionLock {
47 Global {
48 _guard: RwLockWriteGuard<'static, ()>,
49 },
50 Local {
51 _guard: RwLockReadGuard<'static, ()>,
52 },
53}
54
55impl Drop for SessionLock {
56 fn drop(&mut self) {
57 SESSION_ACTIVE.set(false);
58 }
59}
60
61struct Patch {
62 entry: usize,
63 address: usize,
64 original: Vec<u8>,
65 replacement: Vec<u8>,
66 #[cfg(target_arch = "aarch64")]
67 _relay: Option<executable::Executable>,
68}
69
70struct OwnedMock {
71 control: Arc<dyn expectation::Control>,
72 detach: Option<Box<dyn FnOnce() + Send>>,
73}
74
75impl Drop for OwnedMock {
76 fn drop(&mut self) {
77 (self.detach.take().expect("mock was already detached"))();
78 self.control.deactivate();
79 }
80}
81
82/// A scope for global or thread-local function mocks.
83///
84/// Drop removes mocks and restores original behavior, including during a panic.
85/// A session cannot move to another thread.
86#[must_use = "keep the session alive while using the mocks"]
87pub struct Session {
88 patches: Vec<Patch>,
89 mocks: Vec<OwnedMock>,
90 local_patches: Vec<usize>,
91 global_routes: Vec<usize>,
92 lock: SessionLock,
93}
94
95impl Session {
96 #[doc(hidden)]
97 pub fn __borrow(&mut self) -> &mut Self {
98 self
99 }
100
101 /// Opens a thread-local session. See [`Self::new_local`].
102 // Opening a session takes a lock and may wait or panic, so it is not a default value.
103 #[allow(clippy::new_without_default)]
104 #[track_caller]
105 pub fn new() -> Self {
106 Self::new_local()
107 }
108
109 /// Opens a global session. Mocks affect all threads.
110 /// Waits for other threads' sessions to end.
111 ///
112 /// # Panics
113 ///
114 /// Panics if this thread already has a session.
115 #[track_caller]
116 pub fn new_global() -> Self {
117 error::check(Self::open(false, true))
118 }
119
120 /// Opens a session whose mocks affect only the current thread.
121 /// Other threads keep their own mocks or call the original function.
122 /// Only one local session may be active per thread. Global sessions exclude
123 /// local sessions, so this waits for them to end. Stop target calls during
124 /// the first installation. Later local installs and cleanup do not patch code.
125 ///
126 /// # Panics
127 ///
128 /// Panics if this thread already has a session.
129 #[track_caller]
130 pub fn new_local() -> Self {
131 error::check(Self::open(true, true))
132 }
133
134 /// Opens a local session without waiting for an active global session.
135 /// Returns [`Error::Busy`] instead of waiting or panicking.
136 pub fn try_new_local() -> Result<Self, Error> {
137 Self::open(true, false)
138 }
139
140 /// Opens a global session without waiting for other sessions.
141 /// Returns [`Error::Busy`] instead of waiting or panicking.
142 pub fn try_new_global() -> Result<Self, Error> {
143 Self::open(false, false)
144 }
145
146 fn open(local: bool, wait: bool) -> Result<Self, Error> {
147 if SESSION_ACTIVE.get() {
148 return Err(Error::Busy);
149 }
150 let lock = if local {
151 let result = if wait {
152 SESSION.read().map_err(TryLockError::Poisoned)
153 } else {
154 SESSION.try_read()
155 };
156 let guard = match result {
157 Ok(guard) => guard,
158 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
159 Err(TryLockError::WouldBlock) => return Err(Error::Busy),
160 };
161 SessionLock::Local { _guard: guard }
162 } else {
163 let result = if wait {
164 SESSION.write().map_err(TryLockError::Poisoned)
165 } else {
166 SESSION.try_write()
167 };
168 let guard = match result {
169 Ok(guard) => guard,
170 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
171 Err(TryLockError::WouldBlock) => return Err(Error::Busy),
172 };
173 SessionLock::Global { _guard: guard }
174 };
175 SESSION_ACTIVE.set(true);
176 Ok(Self {
177 patches: Vec::new(),
178 mocks: Vec::new(),
179 local_patches: Vec::new(),
180 global_routes: Vec::new(),
181 lock,
182 })
183 }
184
185 #[doc(hidden)]
186 pub fn __thread(&self) -> Option<ThreadId> {
187 matches!(self.lock, SessionLock::Local { .. }).then(|| std::thread::current().id())
188 }
189
190 #[doc(hidden)]
191 /// # Safety
192 /// All three pointers must use the checked source signature and stay loaded
193 /// until process exit. Stop calls during the first installation.
194 pub unsafe fn __replace_local(
195 &mut self,
196 source: *const (),
197 dispatcher: *const (),
198 target: *const (),
199 ) -> Result<(), Error> {
200 self.local_patches.reserve(1);
201 // SAFETY: the macro checks the function types before calling this method.
202 unsafe {
203 routing::install_replacement(source as usize, dispatcher as usize, target as usize)?
204 };
205 self.local_patches.push(source as usize);
206 Ok(())
207 }
208
209 /// Installs a replacement at a raw function entry point.
210 ///
211 /// Prefer [`replace!`] to check function signatures at compile time.
212 ///
213 /// # Safety
214 ///
215 /// Both pointers must name live functions with matching signatures, ABIs,
216 /// and lifetimes for every caller. Keep their code loaded and unchanged
217 /// until restored. No thread may run the patched bytes during installation
218 /// or restoration, including drop. No branch may enter those bytes midway.
219 /// Calls must reach the source entry. Inlined or merged calls cannot be isolated.
220 /// Do not replace functions used by shimforge or its memory and OS code.
221 /// The replacement must meet the safety rules its callers rely on.
222 ///
223 /// # Panics
224 ///
225 /// Panics if the session is local or the replacement cannot be installed.
226 #[track_caller]
227 pub unsafe fn replace_raw(&mut self, source: *const (), target: *const ()) {
228 // SAFETY: the caller follows this method's safety contract.
229 error::check(unsafe { self.install_raw(source, target) });
230 }
231
232 /// Installs a raw replacement under the rules of [`Self::replace_raw`].
233 unsafe fn install_raw(&mut self, source: *const (), target: *const ()) -> Result<(), Error> {
234 if self.__thread().is_some() {
235 return Err(Error::Expectation(
236 "use mock! or mock_async in a local session".into(),
237 ));
238 }
239 let source = source as usize;
240 let target = target as usize;
241 if source == target {
242 return Err(Error::SameAddress);
243 }
244 // Check for aliases and cycles before decoding patched bytes.
245 if self.patches.iter().any(|patch| {
246 let end = patch.address + patch.original.len();
247 (patch.entry <= source && source < end) || (patch.entry <= target && target < end)
248 }) {
249 return Err(Error::Overlap);
250 }
251 self.global_routes.reserve(1);
252 if routing::global(source, target)? {
253 self.global_routes.push(source);
254 return Ok(());
255 }
256 memory::read(target, 1)?;
257 let bytes = memory::read(source, code::MAX_PREFIX)?;
258 #[cfg(target_arch = "aarch64")]
259 let relay = {
260 let mut relay = executable::Executable::near(source)?;
261 relay.publish(&code::jump(relay.address(), target)?)?;
262 relay
263 };
264 #[cfg(target_arch = "aarch64")]
265 let target = relay.address();
266 let plan = code::plan(source, target, &bytes)?;
267 let address = source.checked_add(plan.offset).ok_or(Error::InvalidRange)?;
268 let end = address
269 .checked_add(plan.original.len())
270 .ok_or(Error::InvalidRange)?;
271 if self
272 .patches
273 .iter()
274 .any(|patch| address < patch.address + patch.original.len() && patch.address < end)
275 || routing::overlaps(address, end - address)
276 {
277 return Err(Error::Overlap);
278 }
279 // Reserve space before changing code so push cannot allocate.
280 self.patches.reserve(1);
281 // SAFETY: the caller keeps both functions loaded and idle during the write.
282 unsafe {
283 memory::write(address, &plan.original, &plan.replacement)?;
284 }
285 self.patches.push(Patch {
286 entry: source,
287 address,
288 original: plan.original,
289 replacement: plan.replacement,
290 #[cfg(target_arch = "aarch64")]
291 _relay: Some(relay),
292 });
293 Ok(())
294 }
295
296 /// Checks all call expectations without removing the mocks.
297 ///
298 /// # Panics
299 ///
300 /// Panics if a mock missed expected calls or received a call it rejected.
301 #[track_caller]
302 pub fn verify(&self) {
303 error::check(self.check_expectations());
304 }
305
306 fn check_expectations(&self) -> Result<(), Error> {
307 for mock in &self.mocks {
308 mock.control.verify()?;
309 }
310 Ok(())
311 }
312
313 #[doc(hidden)]
314 /// # Safety
315 /// Follow `replace_raw` rules. Detach must release the matching mock state.
316 pub unsafe fn __install(
317 &mut self,
318 source: *const (),
319 target: *const (),
320 control: Arc<dyn expectation::Control>,
321 detach: Box<dyn FnOnce() + Send>,
322 ) -> Result<(), Error> {
323 let mock = OwnedMock {
324 control,
325 detach: Some(detach),
326 };
327 self.mocks.reserve(1);
328 // SAFETY: the caller provides matching functions and keeps them idle.
329 unsafe {
330 if self.__thread().is_some() {
331 self.local_patches.reserve(1);
332 routing::install(source as usize, target as usize)?;
333 self.local_patches.push(source as usize);
334 } else {
335 self.install_raw(source, target)?;
336 }
337 }
338 self.mocks.push(mock);
339 Ok(())
340 }
341
342 /// Removes all mocks, then checks their expectations.
343 ///
344 /// Stop global target calls as required by [`Self::replace_raw`].
345 ///
346 /// # Panics
347 ///
348 /// Panics if original code cannot be restored. The failed patch stays in the
349 /// session, so a later `restore` or the drop retries it. Also panics, after all
350 /// mocks are removed, if an expectation failed.
351 #[track_caller]
352 pub fn restore(&mut self) {
353 error::check(self.restore_patches());
354 let result = self.check_expectations();
355 self.detach();
356 error::check(result);
357 }
358
359 fn detach(&mut self) {
360 self.mocks.clear();
361 }
362
363 fn restore_patches(&mut self) -> Result<(), Error> {
364 while let Some(source) = self.local_patches.last() {
365 routing::remove(*source);
366 self.local_patches.pop();
367 }
368 while let Some(source) = self.global_routes.pop() {
369 routing::remove_global(source);
370 }
371 while let Some(patch) = self.patches.last() {
372 // SAFETY: replace_raw requires the target to stay loaded and idle here.
373 unsafe {
374 memory::write(patch.address, &patch.replacement, &patch.original)?;
375 }
376 self.patches.pop();
377 }
378 Ok(())
379 }
380}
381
382impl Drop for Session {
383 fn drop(&mut self) {
384 finish(self.restore_patches(), std::process::abort);
385 let result = self.check_expectations();
386 self.detach();
387 if !std::thread::panicking() {
388 if let Err(error) = result {
389 panic!("{error}");
390 }
391 }
392 }
393}
394
395/// Creates a mock with argument matching and checked call counts.
396///
397/// ```
398/// fn read_count(key: &str) -> usize { key.len() }
399/// let mut session = shimforge::Session::new();
400/// let mock = shimforge::mock!(session, read_count, fn(&str) -> usize);
401/// mock.expect().with(|key| *key == "orders").once().returns(12);
402/// assert_eq!(read_count("orders"), 12);
403/// ```
404///
405/// Matchers borrow arguments. Return closures may capture owned values and must
406/// be `Send + 'static`. Follow the same runtime safety rules as [`replace!`].
407/// Installation panics if the function cannot be patched.
408///
409/// Borrowed inputs in a safe signature must accept any lifetime. For a function
410/// that only accepts `'static` borrows, declare an `unsafe fn` signature; it is
411/// checked only as a function pointer, so confirm the lifetimes yourself.
412///
413/// The source signature must match:
414/// ```compile_fail
415/// let mut session = shimforge::Session::new_global();
416/// fn source(value: u64) -> u64 { value }
417/// shimforge::mock!(session, source, fn(u32) -> u32);
418/// ```
419/// A replacement cannot require a longer input borrow:
420/// ```compile_fail
421/// let mut session = shimforge::Session::new_global();
422/// fn source(value: &str) -> usize { value.len() }
423/// shimforge::mock!(session, source, fn(&'static str) -> usize);
424/// ```
425/// Type aliases do not bypass this check:
426/// ```compile_fail
427/// let mut session = shimforge::Session::new_global();
428/// fn source(value: &str) -> usize { value.len() }
429/// type Input = &'static str;
430/// shimforge::mock!(session, source, fn(Input) -> usize);
431/// ```
432/// A function that only accepts `'static` borrows needs an `unsafe fn` signature:
433/// ```compile_fail
434/// let mut session = shimforge::Session::new_global();
435/// fn source(value: &'static str) -> usize { value.len() }
436/// shimforge::mock!(session, source, fn(&'static str) -> usize);
437/// ```
438/// A static result cannot become a shorter borrow:
439/// ```compile_fail
440/// let mut session = shimforge::Session::new_global();
441/// fn source(_: &str) -> &'static str { "fixed" }
442/// shimforge::mock!(session, source, fn(&str) -> &str);
443/// ```
444/// A result must stay tied to the same argument:
445/// ```compile_fail
446/// let mut session = shimforge::Session::new_global();
447/// fn source<'a, 'b>(left: &'a str, _: &'b str) -> &'a str { left }
448/// shimforge::mock!(session, source, for<'a, 'b> fn(&'a str, &'b str) -> &'b str);
449/// ```
450/// Caller names cannot shadow the checks:
451/// ```compile_fail
452/// let mut session = shimforge::Session::new_global();
453/// fn __target(_: &str) -> &'static str { "fixed" }
454/// shimforge::mock!(session, __target, fn(&str) -> &str);
455/// ```
456/// Captures must be safe to send between threads:
457/// ```compile_fail
458/// let mut session = shimforge::Session::new_global();
459/// fn source() -> usize { 1 }
460/// let mock = shimforge::mock!(session, source, fn() -> usize);
461/// let value = std::rc::Rc::new(2);
462/// mock.expect().returning(move || *value);
463/// ```
464/// Captures must outlive the test's stack:
465/// ```compile_fail
466/// let mut session = shimforge::Session::new_global();
467/// fn source() -> usize { 1 }
468/// let mock = shimforge::mock!(session, source, fn() -> usize);
469/// let value = String::from("token");
470/// mock.expect().returning(|| value.len());
471/// ```
472/// Return closures cannot create dangling references:
473/// ```compile_fail
474/// let mut session = shimforge::Session::new_global();
475/// fn source(value: &str) -> &str { value }
476/// let mock = shimforge::mock!(session, source, fn(&str) -> &str);
477/// mock.expect().returning(|_| String::from("temporary").as_str());
478/// ```
479/// Calling conventions must match:
480/// ```compile_fail
481/// let mut session = shimforge::Session::new_global();
482/// extern "C" fn source() -> usize { 1 }
483/// shimforge::mock!(session, source, fn() -> usize);
484/// ```
485#[macro_export]
486macro_rules! mock {
487 ($($input:tt)*) => { $crate::__mock!($crate, $($input)*) };
488}
489
490#[doc(hidden)]
491#[macro_export]
492macro_rules! __install_replacement {
493 ($session:expr, $source:expr, $dispatcher:expr, $target:expr) => {{
494 // SAFETY: replace! checks the types before generating this dispatcher.
495 unsafe { $session.__replace_local($source, $dispatcher, $target) }
496 }};
497}
498
499#[doc(hidden)]
500#[macro_export]
501macro_rules! __invoke {
502 ($address:expr, $signature:ty, ($($argument:ident),* $(,)?)) => {{
503 let address = $address;
504 // SAFETY: routing stores only targets with this checked signature.
505 #[allow(clippy::type_complexity)]
506 let function: $signature = unsafe { ::std::mem::transmute(address) };
507 function($($argument),*)
508 }};
509}
510
511#[doc(hidden)]
512#[macro_export]
513macro_rules! __install {
514 ($session:expr, $source:expr, $target:expr, $control:expr, $detach:expr) => {{
515 let (session, source, target, control, detach) =
516 (($session).__borrow(), $source, $target, $control, $detach);
517 // SAFETY: the generated mock checks types; callers keep patching idle.
518 unsafe { session.__install(source, target, control, detach) }
519 }};
520}
521
522fn finish(result: Result<(), Error>, fatal: fn() -> !) {
523 if result.is_err() {
524 // Abort if a patch cannot be removed, even during a panic.
525 fatal();
526 }
527}
528
529/// Replaces a function and checks its signature at compile time.
530///
531/// ```no_run
532/// # fn original(x: i32) -> i32 { x + 1 }
533/// # fn fake(x: i32) -> i32 { x + 10 }
534/// let mut session = shimforge::Session::new_global();
535/// shimforge::replace!(session, original => fake, fn(i32) -> i32);
536/// ```
537///
538/// No `unsafe` block is needed. Follow the crate's safety rules. Lifetime checks
539/// are best effort; do not narrow lifetimes to force a type match. Borrowed inputs
540/// in a safe signature must accept any lifetime; use an `unsafe fn` signature for a
541/// function that only accepts `'static` borrows. Closures without captures are
542/// accepted. Installation panics if the function cannot be patched.
543///
544/// Incompatible signatures are rejected:
545/// ```compile_fail
546/// let mut session = shimforge::Session::new_global();
547/// fn source(x: u32) -> u32 { x }
548/// shimforge::replace!(session, source => |x: u64| x, fn(u32) -> u32);
549/// ```
550/// The source must also match:
551/// ```compile_fail
552/// let mut session = shimforge::Session::new_global();
553/// fn source(x: u64) -> u64 { x }
554/// shimforge::replace!(session, source => |x| x, fn(u32) -> u32);
555/// ```
556/// Input borrows cannot be narrowed:
557/// ```compile_fail
558/// let mut session = shimforge::Session::new_global();
559/// fn source(value: &str) -> usize { value.len() }
560/// shimforge::replace!(session, source => |value| value.len(), fn(&'static str) -> usize);
561/// ```
562/// This also applies to mutable borrows:
563/// ```compile_fail
564/// let mut session = shimforge::Session::new_global();
565/// fn source(value: &mut usize) { *value += 1; }
566/// shimforge::replace!(session, source => |_| (), fn(&'static mut usize));
567/// ```
568/// A function that only accepts `'static` borrows needs an `unsafe fn` signature:
569/// ```compile_fail
570/// let mut session = shimforge::Session::new_global();
571/// fn source(value: &'static str) -> usize { value.len() }
572/// shimforge::replace!(session, source => |value| value.len(), fn(&'static str) -> usize);
573/// ```
574/// A static result cannot become a shorter borrow:
575/// ```compile_fail
576/// let mut session = shimforge::Session::new_global();
577/// fn source(_: &str) -> &'static str { "fixed" }
578/// shimforge::replace!(session, source => |value| value, fn(&str) -> &str);
579/// ```
580/// A result must stay tied to the same argument:
581/// ```compile_fail
582/// let mut session = shimforge::Session::new_global();
583/// fn source<'a, 'b>(left: &'a str, _: &'b str) -> &'a str { left }
584/// shimforge::replace!(session, source => |_, right| right,
585/// for<'a, 'b> fn(&'a str, &'b str) -> &'b str);
586/// ```
587/// Calling conventions must match:
588/// ```compile_fail
589/// let mut session = shimforge::Session::new_global();
590/// extern "C" fn source(x: u32) -> u32 { x }
591/// shimforge::replace!(session, source => |x| x, fn(u32) -> u32);
592/// ```
593/// Capturing closures are rejected:
594/// ```compile_fail
595/// let mut session = shimforge::Session::new_global();
596/// let captured = String::from("hello");
597/// fn source() -> usize { 1 }
598/// shimforge::replace!(session, source => || captured.len(), fn() -> usize);
599/// ```
600#[macro_export]
601macro_rules! replace {
602 (@infer $type:ty) => { _ };
603 ($session:expr, $source:expr => $target:expr,
604 $(for<$($lt:lifetime),+>)? fn($($arg:ty),* $(,)?) $(-> $ret:ty)? $(,)?) => {{
605 let original = $source;
606 let target: $(for<$($lt),+>)? fn($($arg),*) $(-> $ret)? = $target;
607 $crate::__check_signature!($source, original, target, $(for<$($lt),+>)? fn($($arg),*) $(-> $ret)?);
608 let source = original as fn($($crate::replace!(@infer $arg)),*) -> _;
609 // Infer source lifetimes, then require matching pointer types.
610 fn checked<T>(source: T, _: T) -> T { source }
611 let source = checked(source, target);
612 let session = ($session).__borrow();
613 if session.__thread().is_some() {
614 $crate::__replace_local!($crate, session, source, target, $(for<$($lt),+>)? fn($($arg),*) $(-> $ret)?)
615 } else {
616 // SAFETY: the types above match; callers follow the runtime safety rules.
617 unsafe { session.replace_raw(source as *const (), target as *const ()) }
618 }
619 }};
620 ($session:expr, $source:expr => $target:expr,
621 $(for<$($lt:lifetime),+>)? unsafe fn($($arg:ty),* $(,)?) $(-> $ret:ty)? $(,)?) => {{
622 let source = $source as unsafe fn($($crate::replace!(@infer $arg)),*) -> _;
623 let target: $(for<$($lt),+>)? unsafe fn($($arg),*) $(-> $ret)? = $target;
624 // Infer source lifetimes, then require matching pointer types.
625 fn checked<T>(source: T, _: T) -> T { source }
626 let source = checked(source, target);
627 let session = ($session).__borrow();
628 if session.__thread().is_some() {
629 $crate::__replace_local!($crate, session, source, target, $(for<$($lt),+>)? unsafe fn($($arg),*) $(-> $ret)?)
630 } else {
631 // SAFETY: the types above match; callers follow the runtime safety rules.
632 unsafe { session.replace_raw(source as *const (), target as *const ()) }
633 }
634 }};
635 ($session:expr, $source:expr => $target:expr,
636 $(for<$($lt:lifetime),+>)? extern $abi:literal fn($($arg:ty),* $(,)?) $(-> $ret:ty)? $(,)?) => {{
637 let source = $source as extern $abi fn($($crate::replace!(@infer $arg)),*) -> _;
638 let target: $(for<$($lt),+>)? extern $abi fn($($arg),*) $(-> $ret)? = $target;
639 // Infer source lifetimes, then require matching pointer types.
640 fn checked<T>(source: T, _: T) -> T { source }
641 let source = checked(source, target);
642 let session = ($session).__borrow();
643 if session.__thread().is_some() {
644 $crate::__replace_local!($crate, session, source, target, $(for<$($lt),+>)? extern $abi fn($($arg),*) $(-> $ret)?)
645 } else {
646 // SAFETY: the types above match; callers follow the runtime safety rules.
647 unsafe { session.replace_raw(source as *const (), target as *const ()) }
648 }
649 }};
650 ($session:expr, $source:expr => $target:expr,
651 $(for<$($lt:lifetime),+>)? unsafe extern $abi:literal fn($($arg:ty),* $(,)?) $(-> $ret:ty)? $(,)?) => {{
652 let source = $source as unsafe extern $abi fn($($crate::replace!(@infer $arg)),*) -> _;
653 let target: $(for<$($lt),+>)? unsafe extern $abi fn($($arg),*) $(-> $ret)? = $target;
654 // Infer source lifetimes, then require matching pointer types.
655 fn checked<T>(source: T, _: T) -> T { source }
656 let source = checked(source, target);
657 let session = ($session).__borrow();
658 if session.__thread().is_some() {
659 $crate::__replace_local!($crate, session, source, target, $(for<$($lt),+>)? unsafe extern $abi fn($($arg),*) $(-> $ret)?)
660 } else {
661 // SAFETY: the types above match; callers follow the runtime safety rules.
662 unsafe { session.replace_raw(source as *const (), target as *const ()) }
663 }
664 }};
665}