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:

  1. Make it safe, coding very carefully.
  2. 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’t Send, when using the multithreaded scheduler. I’ll need to understand Send better instead of just unsafely slapping it onto the stream.
  • Multiple readers for a stream, to remove the need for the Tee block. This doesn’t seem hard, but it does mean complicating the unsafe 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 the borrow-instead-of-refcount branch, but it has two drawbacks:
    1. The application API becomes a bit worse, because the application now has to own all the streams, not just the blocks.
    2. 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.