Proposal for how to unblock use cases that involve TypeId of potentially non-'static types, while exposing zero of the risks that took down the previous attempt at this.
Background
Type id is currently exposed in the standard library as follows:
// in core::any⸺
pub struct TypeId {…}
impl TypeId {
pub fn of<T>() -> Self
where
T: 'static + ?Sized;
}
impl Copy, Clone, Eq, Ord, Debug, Hash
// in core::intrinsics⸺
#[unstable]
pub extern "rust-intrinsic" fn type_id<T>() -> u64
where
T: 'static + ?Sized;
RFC 1849 proposed deleting the T: 'static
bound from the above. This RFC was accepted by the lang team in 2017, and then unaccepted in 2020 in #41875 without ever having been implemented, attributed to "potential for confusion and misuse".
The concern is that since lifetimes are erased at runtime in Rust, &'a U
and &'b U
necessarily have the same TypeId
regardless of lifetimes. Thus pretty much any use of TypeId
with non-'static types where downcasting is involved is going to be unsound.
Counterproposal
My pre-RFC proposes the following API:
// in core::any⸺
pub struct TypeId {…}
impl TypeId {
pub fn of<T>() -> Self
where
T: 'static + ?Sized,
{
TypeId(intrinsics::type_id::<T>())
}
pub fn same_as<T>(&self) -> bool
where
T: ?Sized,
{
intrinsics::all_types_with_this_id_are_static::<T>()
&& self.0 == intrinsics::type_id::<T>()
}
}
// in core::intrinsics⸺
#[unstable]
pub extern "rust-intrinsic" fn type_id<T>() -> u64
where
T: ?Sized;
#[unstable]
pub extern "rust-intrinsic" fn all_types_with_this_id_are_static<T>() -> bool
where
T: ?Sized;
But is this useful? I still see a 'static
bound…
Yes! This API would be amazing for Serde.
In Serde, data formats contain code that is generic over what type is being deserialized or serialized, and those generics usually do not have a 'static bound because plenty of non-'static types can be deserialized and serialized.
However, data formats often want to implement special behavior for a small set of special types unique to that data format, for example DateTime in TOML.
fn part_of_deserializer<'de, V>(data: D, visitor: V) -> Result<V::Value>
where
V: serde::de::Visitor<'de>,
{
if /** V == DateTimeVisitor<Value = DateTime> */ {
let datetime: DateTime = deserialize_datetime(data)?;
Ok(unsafe {
mem::transmute_copy(&ManuallyDrop::new(datetime))
})
} else {
deserialize_anything_else(data, visitor)
}
}
This behavior is impossible to express today. With the API from the pre-RFC, the condition becomes implementable as:
if TypeId::of::<DateTimeVisitor>().same_as::<V>() {
But is this risky to expose?
No! You still cannot get a TypeId
for a type that is not 'static
!
You can only get a TypeId
of a type that is statically 'static
, and then check whether some other type (which may or may not be 'static
) is the same as the first one and has the same 'static
lifetime.
Examples
struct StaticStr(&'static str);
struct BorrowedStr<'a>(&'a str);
// true
assert!(TypeId::of::<StaticStr>().same_as::<StaticStr>());
// false because different type lol
assert!(! TypeId::of::<StaticStr>().same_as::<&'static str>());
// false because all_types_with_this_id_are_static is false
assert!(! TypeId::of::<BorrowedStr<'static>>().same_as::<BorrowedStr<'a>>());