Libraries / Interoperability Guidelines
Types are Send (M-TYPES-SEND)
Public types should be Send
for compatibility reasons:
- All futures produced (explicitly or implicitly) must be
Send
- Most other types should be
Send
, but there might be exceptions
Futures
When declaring a future explicitly you should ensure it is, and remains, Send
.
#![allow(unused)] fn main() { use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; struct Foo {} impl Future for Foo { // Explicit implementation of `Future` for your type type Output = (); fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<<Self as Future>::Output> { todo!() } } // You should assert your type is `Send` const fn assert_send<T: Send>() {} const _: () = assert_send::<Foo>(); }
When returning futures implicitly through async
method calls, you should make sure these are Send
too.
You do not have to test every single method, but you should at least validate your main entry points.
#![allow(unused)] fn main() { async fn foo() { } // TODO: We want this as a macro as well fn assert_send<T: Send>(_: T) {} _ = assert_send(foo()); }
Regular Types
Most regular types should be Send
, as they otherwise infect futures turning them !Send
if held across .await
points.
#![allow(unused)] fn main() { use std::rc::Rc; async fn read_file(x: &str) {} async fn foo() { let rc = Rc::new(123); // <-- Holding this across an .await point prevents read_file("foo.txt").await; // the future from being `Send`. dbg!(rc); } }
That said, if the default use of your type is instantaneous, and there is no reason for it to be otherwise held across .await
boundaries, it may be !Send
.
#![allow(unused)] fn main() { use std::rc::Rc; struct Telemetry; impl Telemetry { fn ping(&self, _: u32) {} } fn telemetry() -> Telemetry { Telemetry } async fn read_file(x: &str) {} async fn foo() { // Here a hypothetical instance Telemetry is summoned // and used ad-hoc. It may be ok for Telemetry to be !Send. telemetry().ping(0); read_file("foo.txt").await; telemetry().ping(1); } }
The Cost of Send Ideally, there would be abstractions that are
Send
in work-stealing runtimes, and!Send
in thread-per-core models based on non-atomic types likeRc
andRefCell
instead.Practically these abstractions don't exist, preventing Tokio compatibility in the non-atomic case. That in turn means you would have to "reinvent the world" to get anything done in a thread-per-core universe.
The good news is, in most cases atomics and uncontended locks only have a measurable impact if accessed more frequently than every 64 words or so.
Working with a large
Vec<AtomicUsize>
in a hot loop is a bad idea, but doing the occasional uncontended atomic operation from otherwise thread-per-core async code has no performance impact, but gives you widespread ecosystem compatibility.
Native Escape Hatches (M-ESCAPE-HATCHES)
Types wrapping native handles should provide unsafe
escape hatches. In interop scenarios your users might have gotten a native handle from somewhere
else, or they might have to pass your wrapped handle over FFI. To enable these use cases you should provide unsafe
conversion methods.
#![allow(unused)] fn main() { type HNATIVE = *const u8; pub struct Handle(HNATIVE); impl Handle { pub fn new() -> Self { // Safely creates handle via API calls todo!() } // Constructs a new Handle from a native handle the user got elsewhere. // This method should then also document all safety requirements that // must be fulfilled. pub unsafe fn from_native(native: HNATIVE) -> Self { Self(native) } // Various extra methods to permanently or temporarily obtain // a native handle. pub fn into_native(self) -> HNATIVE { self.0 } pub fn to_native(&self) -> HNATIVE { self.0 } } }
Don't Leak External Types (M-DONT-LEAK-TYPES)
Where possible, you should prefer std
1 types in public APIs over types coming from external crates. Exceptions should be carefully considered.
Any type in any public API will become part of that API's contract. Since std
and constituents are the only crates
shipped by default, and since they come with a permanent stability guarantee, their types are the only ones that come without an interoperability risk.
A crate that exposes another crate's type is said to leak that type.
For maximal long term stability your crate should, theoretically, not leak any types. Practically, some leakage is unavoidable, sometimes even beneficial. We recommend you follow this heuristic:
- if you can avoid it, do not leak third-party types
- if you are part of an umbrella crate,2 you may freely leak types from sibling crates.
-
behind a relevant feature flag, types may be leaked (e.g.,
serde
) - without a feature only if they give a substantial benefit. Most commonly that is interoperability with significant other parts of the Rust ecosystem based around these types.
1 In rare instances, e.g., high performance libraries used from embedded, you might even want to limit yourself to core
only.
2 For example, a runtime
crate might be the umbrella of runtime_rt
, runtime_app
and runtime_clock
As users are
expected to only interact with the umbrella, siblings may leak each others types.