-
Notifications
You must be signed in to change notification settings - Fork 90
Rust guest async bindings: low-overhead streams with no copies difficult to make memory safe #471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Just to try to understand the constraint space: is there any safe way to have an async function that borrows a mutable slice? |
Sort of, but from the component model's perspective I don't think so. In Rust there's nothing wrong with this signature for example: async fn foo(a: &mut [u8]) { /* ... */ } The requirements here are specifically:
The "goes out of scope" typically means that something dropped it (or consumed it, etc). One way of going out of scope though is being leaked, and leaking notably does not run any destructor for So ideally something like this: world foo {
import f: func(x: list<u8>);
} would show up in Rust as: async fn f(x: &[u8]) { /* ... */ } The problem is that the pointer here is handed off to the component model inside of The crux of this is basically that a pointer is handed off to some external system, and the safety of the operation relies on the fact that the operation can be reliably cancelled if necessary, and that can't happen in Rust 100% of the time. It'll be right 99% of the time because leaks generally don't happen, but in terms of default/quality bindings I'd want to reach 100%. To the best of my knowledge this isn't a major problem in the Rust async ecosystem since APIs like io_uring or overlapped I/O are deep within runtimes and never exposed "raw" to users (or when they are they're appropriately The only solution I can think of is to change the bindings mode for all async APIs to requiring "owned" variants, meaning above the bindings would show up as: async fn f(x: Vec<u8>) { /* ... */ } The ramifications of a signature like this are that dealing with buffers is far less efficient as you have to copy something out just to give it to a function and there's no easy way to reclaim the buffer after |
We can also support e.g. |
Ohhhh, so just to help me bottom out here: is the critical detail that makes |
That's true yeah, but doesn't generalize to
Correct yeah.
To the best of my Rust knowledge of modern idioms, you're correct that the only sound way to implement this is to copy data. Using Another way of putting this is that it's impossible to create a un-leakable type in Rust right now, every type may be leaked. While this is somewhat orthogonal, I want to also clarify one thing lest anyone in the future read this thread and conclude that Rust is fundamentally broken. The Rust compiler won't randomly leak values for example, and Rust, again to the best of my knowledge, guarantees a few things around destructors:
That's why everything generally works in Rust with no leaks as everything is rooted eventually in the stack most of the time. For the 1% of the time this is a problem there are specific APIs in Rust to leak values which aren't commonly used but are often unsafe building blocks that can be accidentally mis-used. The other poster-child for leaking values where you don't opt-in to leaking values is a |
Indeed, it's actually quite hard to accidentally leak memory in Rust. You usually have to do it explicitly by using an API like The only time where you might accidentally leak something is with an |
Setting |
Unfortunately no, a return-pointer is also passed which, if not cancelled, will be filled in asynchronously and possibly corrupt memory Rust things was un-writable by this future. @vados-cosmonic, @yoshuawuyts, @dicej, and I talked about this in depth today as well and I also wanted to jot down notes from our discussion. The general conclusion we came to was:
|
I believe |
I'm going to close this as I think we've figured out what to do on the Rust bindings side of things and nothing is going to change in the upstream spec as a result of this. If Rust changes, however, to get |
Whether or not we'll change anything as a result of this, I'm not sure. In thinking more about how bindings will work in Rust I'm realizing that many possible bindings to futures/streams are all memory-unsafe in Rust and would require
unsafe
(which is something we want to avoid). The tl;dr; is:stream<u8>
that looks something along the lines ofasync fn write(&mut self, bytes: &[u8]) -> Result<()>
which implicitly borrowsbytes
for the duration of the entireasync
function.Effectively all interaction with futures/streams will have to take ownership of buffers temporarily while the operation is in progress. AFAIK that's pretty un-idiomatic in Rust and gets quite cumbersome, but there's effectively no other option for memory-safe code. It basically means that the Rust APIs will have to get opinionated very quickly which isn't a great sign for foundational APIs.
There's not really anything that can be done about this at the component model level apart from radically redesigning things which is more-or-less off the table at this point. Otherwise I mostly wanted to note this down as a consequence of an io-uring style API (which AFAIK io-uring has no low-level safe bindings in Rust as well, probably for similar reasons)
The text was updated successfully, but these errors were encountered: