Skip to main content

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}