Skip to content

Conversation

@luisholanda
Copy link
Contributor

This PR changes how we generate the Server traits for interfaces. We now generate async fn compatible traits instead of methods returning Promise.

The methods now receive &self instead of &mut self, which causes server implementations to need RefCell/Cell for mutable state, similar to what we have for Send servers in other RPC systems. This is slightly different from what was discussed in #577, because we don't actually have a Rc<impl Server> inside Client, but instead Rc<ServerDispatch>, making the receiver a Rc<Self> (which would allow easier background processing spawning) would require an extra allocation, pointer chasing, and ref-counting operations.

From the changes in the examples, we can see that the code is significantly easier to understand, with pry! rarely being needed. The only case I truly needed to use it was when the code needed to manually create a Promise due to a RefMut preventing the use of async fn (which could cause double borrows). For me, this is a significant improvement in UX.

In matters of breaking changes, this, of course, breaks every pre-existing implementation, but it should be an easy migration. In the crates themselves, capnp::capability::Server needed to be changed to receive Rc<Self> instead of &mut self, not only the &mut isn't necessary anymore but also receiving a Rc<Self> (which we already have where it is called) also allow us to make the generated methods for Server traits non-'static, which makes the async fn syntax significantly more useful. Additionally, certain methods in CapabilityServerSet, that exposed the internal Rc<RefCell<Dispatcher>> from Client, needed to be changed to return Rc<Dispatcher>. These changes in the crates were the main reason why there isn't a switch for old/new code generation methods, as they are incompatible with each other.

Also, given that we're using async/await in the generated code, they can't be compiled in editions 2015/2018 anymore. It may be possible to support them with some added complexity in the generated code, but I don't think it is worth it.

Closes: #577

server_interior.push(
Line(fmt!(ctx,
"fn {}(&mut self, _: {}Params<{}>, _: {}Results<{}>) -> {capnp}::capability::Promise<(), {capnp}::Error> {{ {capnp}::capability::Promise::err({capnp}::Error::unimplemented(\"method {}::Server::{} not implemented\".to_string())) }}",
"fn {}(&self, _: {}Params<{}>, _: {}Results<{}>) -> impl ::std::future::Future<Output = Result<(), {capnp}::Error>> + '_ {{ async {{ Err({capnp}::Error::unimplemented(\"method {}::Server::{} not implemented\".to_string())) }} }}",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all the code is single-threaded, we could've used async fn directly in the definition, making the code a tiny bit easier to read. This would require us to allow(async_fn_in_traits) though, so I decided to keep it as impl Future.

@dwrensha
Copy link
Member

Wow! Thank you for putting this together. Reviewing this will be a high priority for me.

@luisholanda
Copy link
Contributor Author

It seems I forgot to check some stuff locally! My bad :(

I'll fix CI tomorrow (currently kinda of late here 😅).

@dwrensha dwrensha added the breaking change requires version bump label Oct 23, 2025
@kjuulh
Copy link

kjuulh commented Oct 23, 2025

It looks really clean! amazing work @luisholanda Love to see fewer pry!s in the example code 🚀

One comment I have is on exposing Rc, RefCell etc. as part of the api; Client, ImbuedMessageBuilder. It is fairly unergonomic and we are now exposing the entire surface area of Rc as an example, small as they may be. I'd rather sacrifice a bit of performance to instead have a small wrapper abstraction (inner pattern?), that takes care of the mutability / reference counting. Strictly from an application writers perspective, I'd rather avoid having to deal with Rc, RefCell etc, if I can. It is probably a separate issue though.

More generally is that we're breaking clients for certain, are there other breaking changes that should be batched in?

@dwrensha
Copy link
Member

Strictly from an application writers perspective, I'd rather avoid having to deal with Rc, RefCell etc, if I can.

What changes in this PR is that now your Server impls need to bring their own interior mutability. So you may need to add some Cell or RefCell wrappers. I don't think you'll need to add any Rc, unless I misunderstand something.

@luisholanda
Copy link
Contributor Author

luisholanda commented Oct 23, 2025

are there other breaking changes that should be batched in?

I've some proposals to improve performance and runtime integration for the capnp-rpc crate, which would require changing its traits, mainly to reduce the number of heap allocations in these lower-layer APIs. My motivation is that I'm using an io_uring-based runtime for my project and need a custom VatNetwork with the lowest possible overhead.

They are still in the design phase, though.

Overall, I would prefer to land this PR in a next branch or similar, so we can work on a 0.3 version of the crates without preventing non-breaking changes from being sent to 0.2.

@dwrensha
Copy link
Member

Overall, I would prefer to land this PR in a next branch or similar, so we can work on a 0.3 version of the crates without preventing non-breaking changes from being sent to 0.2.

My plan was to switch the version to 0.22.0-alpha before landing any breaking changes. Then we can always make a 0.21 branch for backporting any non-breaking fixes that come up.

@dwrensha
Copy link
Member

My motivation is that I'm using an io_uring-based runtime for my project and need a custom VatNetwork with the lowest possible overhead.

I think the improvements in this PR are important enough that we should not block them on further speculative changes.

@luisholanda
Copy link
Contributor Author

@dwrensha, I've updated the PR, fixed the CI, and applied your suggestions to the code.

With this commit changes how we generate the `Server` traits for
interfaces. We now generate `async fn` compatible traits instead
of methods returning `Promise`.

The methods now receive `&self` instead of `&mut self`, which
causes server implementations to need `RefCell`/`Cell` for mutable
state, similar to what we have for `Send` servers in other RPC systems.
This is slightly different from what was discussed in capnproto#577, because
we don't actually have a `Rc<impl Server>` inside `Client`, but instead
`Rc<ServerDispatch>`, making the receiver a `Rc<Self>` (which would
allow easier background processing spawning) would require an extra
allocation, pointer chasing, and ref-counting operations.

From the changes in the examples, we can see that the code is
significantly easier to understand, and `pry!` is almost never
needed. The only case I truly needed to use it was when the code
needed to manually create `Promise` due to a `RefMut` preventing
use to use `async fn` (which could cause double borrows).

In matters of breaking changes, this of course breaks every pre-existing
implementation, but it should be an easy migration. In the crates
themselves, `capnp::capability::Server` needed to be changed to receive
`Rc<Self>` instead of `&mut self`, not only the `&mut` isn't necessary
anymore but also receiving a `Rc<Self>` (which we already have where it
is called) also allow us to make the generated methods for `Server` traits
non-'static, which makes the `async fn` syntax significantly more
useful. Also, some methods in `CapabilityServerSet` which exposed the
internal `Rc<RefCell<Dispatcher>>` from `Client` needed to be changed
to return just `Rc<Dispatcher>`.

Also, given that we're using `async/await` in the generated code, they
can't be compiled in editions 2015/2018 anymore. It may be possible to
support them with some added complexity in the generated code, but I
don't think it is worth it.

Closes: capnproto#577
@abronan
Copy link

abronan commented Oct 26, 2025

Love the change btw, really nice work! 👏 Eager to remove all the Promise::from_future(async move { and the abusive cloning from my project. I think the trade-off of having to deal with interior mutability explicitly is fine, since it's still a lot more friendly than what we have currently.

@luisholanda luisholanda force-pushed the async-fn-server-traits branch from e0519f0 to 54d3882 Compare October 26, 2025 20:16
@codecov
Copy link

codecov bot commented Oct 26, 2025

Codecov Report

❌ Patch coverage is 76.16099% with 77 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.63%. Comparing base (ab342b3) to head (54d3882).
⚠️ Report is 205 commits behind head on master.

Files with missing lines Patch % Lines
capnp-rpc/test/impls.rs 77.65% 63 Missing ⚠️
capnpc/src/codegen.rs 0.00% 13 Missing ⚠️
capnp-rpc/test/reconnect_test.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #593      +/-   ##
==========================================
- Coverage   51.64%   50.63%   -1.01%     
==========================================
  Files          69       70       +1     
  Lines       33735    32319    -1416     
==========================================
- Hits        17422    16366    -1056     
+ Misses      16313    15953     -360     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dwrensha dwrensha merged commit 1c2d758 into capnproto:master Oct 26, 2025
10 checks passed
@luisholanda luisholanda deleted the async-fn-server-traits branch October 27, 2025 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change requires version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

async fn methods for RPC Server traits

4 participants