RustRadio improved API 0.4
Since last time, I’ve improved the API a bit. That last post was about API version 0.3. Now it’s on 0.4, and I think it’s getting pretty decent.
0.3 could never have worked very well. The API was VecDeque
-based,
which means it could not provide a linear view (a slice) of all the
data in the buffer.
The 0.4 API is simpler. You get a typed slice, and you read or write to, it as appropriate. Because all streams are currently single writer, single reader, the code is simple, and requires minimal amount of locking.
It’s simpler, but I switched to using memory mapped circular buffers, with a slice as the stream interface. This means that the buffer is allocated only once, yet both reader and writer can use all space available to them, linearly, without having to worry about wrapping around.
The code is still at https://github.com/ThomasHabets/rustradio. I
registered the github org rustyradio
, too. rustradio
was taken. I
sent a message to the owner, since it seems to not have any real
content, but have not heard back.
Unsafe code
To make this multiuser stream I did have to write some unsafe
code,
though. There’s definitely a risk that I made a mistake. unsafe
code
means that the safety is off.
But that’s every day in the life of a C++ programmer. On its trickiest day, Rust is still less tricky than C++ to get right.
I found that there are two ways to get unsafe
code wrong, in
Rust. One is directly, corrupting memory right then and there. The
other is more subtle. If you get it wrong in this other way, you’re
not creating a bug, really, but you do allow other “safe” code to be
buggy.
Like what if you allow handing off two mutable references to the same range?
So you have two jobs, when writing unsafe
code:
- Make it safe, coding very carefully.
- Make it impossible to use incorrectly. You have to collude with the borrow checker police, so that it can do its job.
For example, I need to prevent accidentally opening a stream for writing (requesting the mutable slice range) twice.
For example, this must not be allowed:
let out = self.dst.write_buf()?;
self.dst.write_buf()?.produce(10, &[]);
I did that with a simple one item refcount. Sure, it’s a runtime check, but if the programmer makes this mistake then they’ll probably find out the first time they run it.
I wonder if there’s a more clever way to do it, to have the borrow checker enforce it at compile time.
Another thing I need to prevent is a block continuing to write, even
after it calls produce()
. Because calling produce(10)
means the
slice is no longer valid. The writer must no longer write to the first
10 elements.
let out = self.dst.write_buf()?;
out.produce(10, &[]);
out.slice()[0] = 1;
Having produce()
update the range pointers seems doable, but would
make for a very surprising API.
That’s where Rust really helps. I made the produce()
method consume
the object. It’s a compile time error to use the object after calling
produce()
! And this includes any and all aliases or borrows.
What’s fixed since 0.3?
- Max stream output is now enforced.
- Multithread capable.
- Circular buffers are 2-10x faster than the
VecDeque
in 0.3.
What could be better?
-
Uncopiable streams (PDUs) should probably get their own stream type, to avoid misuse. This would not really be a problem if Rust allowed binding to
!Copy
, but it doesn’t. -
It’d be nice to make it a compile time error to open an input stream for write, and vice versa.
- I tried to do this in the
ReadStream
branch, but I’m not sure how to solve that the compiler complains that the trait isn’tSend
, when using the multithreaded scheduler. I’ll need to understandSend
better instead of just unsafely slapping it onto the stream.
- I tried to do this in the
-
Multiple readers for a stream, to remove the need for the
Tee
block. This doesn’t seem hard, but it does mean complicating theunsafe
code. And I don’t need it at the moment, so I’ll leave it for now. In any case adding that feature won’t break API compatability. - Streams use
Arc
to let both sides of the stream access them. It would be nicer if I could just hand out references. I’m trying it out in theborrow-instead-of-refcount
branch, but it has two drawbacks:- The application API becomes a bit worse, because the application now has to own all the streams, not just the blocks.
- The blocks need a lot of lifetime annotations, in order to hold on to the stream references. Possibly I could have the graph own the streams, solving at least one of these problems.
- A macro (or code repetition) is still needed for block API and graph assembly.