Skip to content

refactor: replace async-trait with native async functions in traits - #441

Merged
otavio merged 1 commit into
masterfrom
drop-async-trait
Aug 19, 2026
Merged

refactor: replace async-trait with native async functions in traits#441
otavio merged 1 commit into
masterfrom
drop-async-trait

Conversation

@otavio

@otavio otavio commented Aug 19, 2026

Copy link
Copy Markdown
Member

Rust stabilized async functions in traits in 1.75 and the MSRV of this workspace is already 1.88, so async-trait no longer earns its place. This drops the dependency and removes all 30 #[async_trait] attributes.

What it buys

  • One less proc-macro in the build graph.
  • No more Box<dyn Future> allocation per call. The state machine paid that on every transition.

Two things the expansion was hiding

StateChangeImpl::handle needs where Self: Sized. A native async function makes a trait dyn-incompatible, and State::inner_state returns &dyn StateChangeImpl. The bound keeps handle out of the vtable and costs nothing, because inner_state only reaches name, is_handling_download and is_preemptive_state.

Download::handle took mut self and never mutated it. The macro moved the binding somewhere unused_mut could not see it.

Notes

  • All but one block used ?Send, which is what a native async function gives on its own. CommunicationState used the Send variant, but nothing needed its future to be Send.
  • async-trait stays in Cargo.lock as an indirect dependency of other crates.

Verification

  • cargo clippy --workspace --all-targets --all-features -- -D warnings after cargo clean of every workspace crate: no errors, no warnings.
  • cargo test: 162 pass, 0 fail.

Rust stabilized async functions in traits in 1.75 and the MSRV is already 1.88,
so the macro carries its own weight for nothing. Removing it drops a proc-macro
from the build graph and the `Box<dyn Future>` that each of the 30 annotated
blocks allocated on every call, which the state machine paid on every
transition.

All but one of those blocks asked for `?Send`, and that is what a native async
function gives on its own. `CommunicationState` used the `Send` variant, but
nothing ever needed its future to be `Send`, so the bound goes away with no
other change.

Two things the expansion was hiding:

`StateChangeImpl::handle` now needs `where Self: Sized`. A native async function
makes a trait dyn-incompatible, and `State::inner_state` returns `&dyn
StateChangeImpl`. The bound keeps `handle` out of the vtable and costs nothing,
because `inner_state` only reaches `name`, `is_handling_download` and
`is_preemptive_state`.

`Download::handle` took `mut self` and never mutated it. The macro moved the
binding somewhere `unused_mut` could not see it.

async-trait stays in Cargo.lock as an indirect dependency of other crates.
@otavio
otavio merged commit 196856d into master Aug 19, 2026
4 of 5 checks passed
@otavio
otavio deleted the drop-async-trait branch August 19, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant