diff --git a/macros/src/signature.rs b/macros/src/signature.rs index d6e87ae..ffe531a 100644 --- a/macros/src/signature.rs +++ b/macros/src/signature.rs @@ -16,9 +16,18 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok .map(|i| format_ident!("__arg{i}", span = Span::mixed_site())) .collect(); let target_name = format_ident!("__target", span = Span::mixed_site()); + // A borrow of this local cannot last for 'static. `&()` would be promoted to a + // 'static borrow and accept every signature. + let local = format_ident!( + "borrowed_inputs_must_accept_any_lifetime", + span = Span::mixed_site() + ); let binder = &signature.lifetimes; let mut probes = TokenStream::new(); - // Probe borrowed inputs without rejecting static-only functions. + // Every borrowed input of the declared signature must accept a borrow of a local + // value, so a rule never treats a short borrow as a longer one. The probe impl is + // chosen by the input's type alone and the local borrow is checked afterwards, so + // no lifetime decides which impl applies and every trait solver agrees. for index in 0..args.len() { for mutable in [false, true] { let name = format_ident!("__Input{index}{}", usize::from(mutable)); @@ -30,36 +39,41 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok .filter_map(|(i, ty)| (i != index).then_some(ty)) .collect(); let mutability = mutable.then(|| quote!(mut)); - let probe_args: Vec<_> = types - .iter() - .enumerate() - .map(|(i, ty)| { - if i == index { - quote!(&'__probe #mutability __Pointee) - } else { - quote!(#ty) - } - }) - .collect(); + let borrowed = |lifetime: TokenStream| -> Vec { + types + .iter() + .enumerate() + .map(|(i, ty)| { + if i == index { + quote!(&#lifetime #mutability __Pointee) + } else { + quote!(#ty) + } + }) + .collect() + }; + let shape_args = borrowed(quote!('__shape)); + let local_args = borrowed(quote!('__local)); probes.extend(quote! { - trait #name<__Pointee: ?Sized, #(#other),*> { - fn #method<__Target>(&self, target: __Target) - where __Target: for<'__probe> __Signature<(#(#probe_args,)*)>; + trait #name<'__shape, __Pointee: ?Sized, #(#other),*> { + fn #method<'__local, __Target>(&self, target: __Target, local: &'__local ()) + where __Pointee: '__local, __Target: __Signature<(#(#local_args,)*)>; } - impl<__Source, __Pointee: ?Sized, #(#other),*> #name<__Pointee, #(#other),*> - for __SourceValue<__Source> - where __Source: for<'__probe> __Signature<(#(#probe_args,)*)> { - fn #method<__Target>(&self, _: __Target) - where __Target: for<'__probe> __Signature<(#(#probe_args,)*)> {} + impl<'__shape, __Shape, __Pointee: ?Sized + '__shape, #(#other),*> #name<'__shape, __Pointee, #(#other),*> + for __Declared<__Shape> + where __Shape: __Signature<(#(#shape_args,)*)> { + fn #method<'__local, __Target>(&self, _: __Target, _: &'__local ()) + where __Pointee: '__local, __Target: __Signature<(#(#local_args,)*)> {} } - trait #fallback { fn #method<__Target>(&self, target: __Target); } - impl<__Source> #fallback for &__SourceValue<__Source> { - fn #method<__Target>(&self, _: __Target) {} + trait #fallback { fn #method<__Target>(&self, target: __Target, local: &()); } + impl<__Shape> #fallback for &__Declared<__Shape> { + fn #method<__Target>(&self, _: __Target, _: &()) {} } - (&__SourceValue(#source)).#method(#target_name); + (&__Declared(#target_name)).#method(#target_name, &#local); }); } } + let local_value = (!args.is_empty()).then(|| quote!(let #local = ();)); quote! { { trait __Signature<__Args> { type Output; } @@ -67,7 +81,7 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok where __Function: ::std::ops::FnOnce(#(#types),*) -> __Output { type Output = __Output; } - struct __SourceValue<__Source>(__Source); + struct __Declared<__Shape>(__Shape); // Mutable references keep the input lifetimes distinct. fn __same_output<__Source, __Target, #(#types),*>(_: __Source, _: __Target, _: (#(&mut #types,)*)) where __Source: __Signature<(#(#types,)*)>, @@ -75,6 +89,7 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok #[allow(clippy::type_complexity)] let _: &mut dyn #binder ::std::ops::FnMut(#(#args),*) = &mut |#(mut #names),*| { let #target_name: #signature = #target; + #local_value #probes __same_output(#source, #target_name, (#(&mut #names,)*)); }; diff --git a/src/lib.rs b/src/lib.rs index 1299c02..6d75ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -406,6 +406,10 @@ impl Drop for Session { /// be `Send + 'static`. Follow the same runtime safety rules as [`replace!`]. /// Installation panics if the function cannot be patched. /// +/// Borrowed inputs in a safe signature must accept any lifetime. For a function +/// that only accepts `'static` borrows, declare an `unsafe fn` signature; it is +/// checked only as a function pointer, so confirm the lifetimes yourself. +/// /// The source signature must match: /// ```compile_fail /// let mut session = shimforge::Session::new_global(); @@ -526,9 +530,10 @@ fn finish(result: Result<(), Error>, fatal: fn() -> !) { /// ``` /// /// No `unsafe` block is needed. Follow the crate's safety rules. Lifetime checks -/// are best effort; do not narrow lifetimes to force a type match. Closures -/// without captures are accepted. Installation panics if the function cannot be -/// patched. +/// are best effort; do not narrow lifetimes to force a type match. Borrowed inputs +/// in a safe signature must accept any lifetime; use an `unsafe fn` signature for a +/// function that only accepts `'static` borrows. Closures without captures are +/// accepted. Installation panics if the function cannot be patched. /// /// Incompatible signatures are rejected: /// ```compile_fail diff --git a/tests/expectations.rs b/tests/expectations.rs index cef7379..c9171a2 100644 --- a/tests/expectations.rs +++ b/tests/expectations.rs @@ -763,7 +763,9 @@ fn independent_input_lifetimes_keep_their_output_link() { fn genuine_static_inputs_and_results_stay_supported() { let _serial = serial_test(); let mut session = Session::new_global(); - let length = mock!(session, static_length, fn(&'static str) -> usize); + // Safe signatures require borrowed inputs that accept any lifetime, so a function + // that only takes 'static borrows is declared unsafe. + let length = mock!(session, static_length, unsafe fn(&'static str) -> usize); let seen = Arc::new(Mutex::new(None)); let captured = seen.clone(); length.expect().once().returning(move |value| {