Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 40 additions & 25 deletions macros/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -30,51 +39,57 @@ 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<TokenStream> {
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; }
impl<__Function, __Output, #(#types),*> __Signature<(#(#types,)*)> for __Function
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,)*)>,
__Target: __Signature<(#(#types,)*), Output = <__Source as __Signature<(#(#types,)*)>>::Output> {}
#[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,)*));
};
Expand Down
11 changes: 8 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tests/expectations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down
Loading