-
Notifications
You must be signed in to change notification settings - Fork 12.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto merge of #97461 - eddyb:proc-macro-less-payload, r=bjorn3
proc_macro: don't pass a client-side function pointer through the server. Before this PR, `proc_macro::bridge::Client<F>` contained both: * the C ABI entry-point `run`, that the server can call to start the client * some "payload" `f: F` passed to that entry-point * in practice, this was always a (client-side Rust ABI) `fn` pointer to the actual function the proc macro author wrote, i.e. `#[proc_macro] fn foo(input: TokenStream) -> TokenStream` In other words, the client was passing one of its (Rust) `fn` pointers to the server, which was passing it back to the client, for the client to call (see later below for why that was ever needed). I was inspired by `@nnethercote's` attempt to remove the `get_handle_counters` field from `Client` (see #97004 (comment)), which combined with removing the `f` ("payload") field, could theoretically allow for a `#[repr(transparent)]` `Client` that mostly just newtypes the C ABI entry-point `fn` pointer <sub>(and in the context of e.g. wasm isolation, that's *all* you want, since you can reason about it from outside the wasm VM, as just a 32-bit "function table index", that you can pass to the wasm VM to call that function)</sub>. <hr/> So this PR removes that "payload". But it's not a simple refactor: the reason the field existed in the first place is because monomorphizing over a function type doesn't let you call the function without having a value of that type, because function types don't implement anything like `Default`, i.e.: ```rust extern "C" fn ffi_wrapper<A, R, F: Fn(A) -> R>(arg: A) -> R { let f: F = ???; // no way to get a value of `F` f(arg) } ``` That could be solved with something like this, if it was allowed: ```rust extern "C" fn ffi_wrapper< A, R, F: Fn(A) -> R, const f: F // not allowed because the type is a generic param >(arg: A) -> R { f(arg) } ``` Instead, this PR contains a workaround in `proc_macro::bridge::selfless_reify` (see its module-level comment for more details) that can provide something similar to the `ffi_wrapper` example above, but limited to `F` being `Copy` and ZST (and requiring an `F` value to prove the caller actually can create values of `F` and it's not uninhabited or some other unsound situation). <hr/> Hopefully this time we don't have a performance regression, and this has a chance to land. cc `@mystor` `@bjorn3`
- Loading branch information
Showing
8 changed files
with
217 additions
and
134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
//! Abstraction for creating `fn` pointers from any callable that *effectively* | ||
//! has the equivalent of implementing `Default`, even if the compiler neither | ||
//! provides `Default` nor allows reifying closures (i.e. creating `fn` pointers) | ||
//! other than those with absolutely no captures. | ||
//! | ||
//! More specifically, for a closure-like type to be "effectively `Default`": | ||
//! * it must be a ZST (zero-sized type): no information contained within, so | ||
//! that `Default`'s return value (if it were implemented) is unambiguous | ||
//! * it must be `Copy`: no captured "unique ZST tokens" or any other similar | ||
//! types that would make duplicating values at will unsound | ||
//! * combined with the ZST requirement, this confers a kind of "telecopy" | ||
//! ability: similar to `Copy`, but without keeping the value around, and | ||
//! instead "reconstructing" it (a noop given it's a ZST) when needed | ||
//! * it must be *provably* inhabited: no captured uninhabited types or any | ||
//! other types that cannot be constructed by the user of this abstraction | ||
//! * the proof is a value of the closure-like type itself, in a sense the | ||
//! "seed" for the "telecopy" process made possible by ZST + `Copy` | ||
//! * this requirement is the only reason an abstraction limited to a specific | ||
//! usecase is required: ZST + `Copy` can be checked with *at worst* a panic | ||
//! at the "attempted `::default()` call" time, but that doesn't guarantee | ||
//! that the value can be soundly created, and attempting to use the typical | ||
//! "proof ZST token" approach leads yet again to having a ZST + `Copy` type | ||
//! that is not proof of anything without a value (i.e. isomorphic to a | ||
//! newtype of the type it's trying to prove the inhabitation of) | ||
//! | ||
//! A more flexible (and safer) solution to the general problem could exist once | ||
//! `const`-generic parameters can have type parameters in their types: | ||
//! | ||
//! ```rust,ignore (needs future const-generics) | ||
//! extern "C" fn ffi_wrapper< | ||
//! A, R, | ||
//! F: Fn(A) -> R, | ||
//! const f: F, // <-- this `const`-generic is not yet allowed | ||
//! >(arg: A) -> R { | ||
//! f(arg) | ||
//! } | ||
//! ``` | ||
|
||
use std::mem; | ||
|
||
// FIXME(eddyb) this could be `trait` impls except for the `const fn` requirement. | ||
macro_rules! define_reify_functions { | ||
($( | ||
fn $name:ident $(<$($param:ident),*>)? | ||
for $(extern $abi:tt)? fn($($arg:ident: $arg_ty:ty),*) -> $ret_ty:ty; | ||
)+) => { | ||
$(pub const fn $name< | ||
$($($param,)*)? | ||
F: Fn($($arg_ty),*) -> $ret_ty + Copy | ||
>(f: F) -> $(extern $abi)? fn($($arg_ty),*) -> $ret_ty { | ||
// FIXME(eddyb) describe the `F` type (e.g. via `type_name::<F>`) once panic | ||
// formatting becomes possible in `const fn`. | ||
assert!(mem::size_of::<F>() == 0, "selfless_reify: closure must be zero-sized"); | ||
|
||
$(extern $abi)? fn wrapper< | ||
$($($param,)*)? | ||
F: Fn($($arg_ty),*) -> $ret_ty + Copy | ||
>($($arg: $arg_ty),*) -> $ret_ty { | ||
let f = unsafe { | ||
// SAFETY: `F` satisfies all criteria for "out of thin air" | ||
// reconstructability (see module-level doc comment). | ||
mem::MaybeUninit::<F>::uninit().assume_init() | ||
}; | ||
f($($arg),*) | ||
} | ||
let _f_proof = f; | ||
wrapper::< | ||
$($($param,)*)? | ||
F | ||
> | ||
})+ | ||
} | ||
} | ||
|
||
define_reify_functions! { | ||
fn _reify_to_extern_c_fn_unary<A, R> for extern "C" fn(arg: A) -> R; | ||
|
||
// HACK(eddyb) this abstraction is used with `for<'a> fn(Bridge<'a>) -> T` | ||
// but that doesn't work with just `reify_to_extern_c_fn_unary` because of | ||
// the `fn` pointer type being "higher-ranked" (i.e. the `for<'a>` binder). | ||
// FIXME(eddyb) try to remove the lifetime from `Bridge`, that'd help. | ||
fn reify_to_extern_c_fn_hrt_bridge<R> for extern "C" fn(bridge: super::Bridge<'_>) -> R; | ||
} |
Oops, something went wrong.