My gamedever wishlist for Rust

I see. But then what prevents you from wrapping the buffer pointer into a struct, which only has setters that memcpy at a particular index? If this setter would just memcpy with the right offset, it should get completely inlined and you should have the same code as if it was in C, no? The only thing needed from the generics would be how the object gets copied, which should probably be handled by memcpy as well, since both [u16] is and [Vertex] are contiguous blocks.

I might be missing some limitation of Rust though, I'm fairly new to it (coming from C++).

What you're suggesting only works for [T] (ie. [Vertex] and [u16]), but not UniformsList because Rust doesn't have pointers to struct members (an equivalent to C++'s &Foo::member where Foo is a struct).

Even for [T] it really feels like cheating because the API is different is it is an array, so it's not truly generic. Plus the fact that writing values by index doesn't really fit well with Rust code in general. You tend to use iterators a lot, and set(array_index, value) is not iterator-friendly.

I guess I could somehow make it work for UniformsList as well with a macro that calls an unsafe function, but again it's very clumbsy.

@darthdeus

To be clear, the problem is essentially about integrating well with Rust and using Rust's type system.

I could just add an unsafe function and call it a day, but in my opinion there should a way for me to do this in safe and idiomatic Rust.

Ah now I understand. I expected from the beginning that you were going to use unsafe pointers, especially since you're dealing with hardware. But it would be interesting to get this kind of control into the type system itself, though it probably might not happen, as it would be very specific.

Isn't that exactly what Makefiles already do? (I'm so new to Rust I don't actually know how cargo works yet, but it seems silly to re-invent Makefiles)

Yes, Cargo serves the same purpose as a Makefile. It does more too, like all the interactions with crates.io.

Regarding the write-only type, I've thought about it and decided to start a repo to supply such a type. It should be fully generic, yet usable in most circumstances. Now I of course have a few questions regarding the design that @tomaka could perhaps help with (because I'd imagine he may become a user of this crate :smile:)

  • Should we rather wrap &mut T or T? Much of the design depends on the answer.
  • How lenient or strict should the API be? Do we allow a pub fn into_inner(self) that unwraps the original value? Or should it be closure based, e.g. fn with_wom<F, T>(t: T, f: F) -> T where F: FnOnce(Wom<T>) (the closure can modify, but not read the T, and the function will return the T afterwards).
  • Should we disallow calling functions on &T (this would rule out the Deref panic! approach, but probably improve usability)?

Note that I've not yet started writing the lint, just the Wom<T> type.

Your design isn't safe because one can still read from a &mut T. Panicking in deref can be easily bypassed by calling deref_mut and reading from the returned value. I prefer writing unsafe code rather than getting an illusion of safety.

That's why I wanted to add a lint that disallows actually reading from Wom<T>. Of course one can still compile without using the lint, but I guess our users should not be too averse to keeping their code from crashing, so I don't envisage an airtight design here. The panic! in Deref is only there as a best effort to fail before actually invoking the unsafe behavior.

Note that a truly safe design is not possible within the current definition of Rust.

That would force the users of my library to use the lint in their code if they want to be safe. It's not great.

Why not? Unless we can get true write-only types, I think it's the best we can do. Also I like how only the original developer needs to run the lint to check their code, whoever compiles or uses that code in the future doesn't need the lint.

That said, if we find a different design (e.g. something Cursor-like) that maintains safety without needing the lint and without excessive runtime cost, I'm all for it.

I don't see how using a lint that the person who writes the code has to use is better than an unsafe block.

This does not need to be an exclusive or – I see those options as being orthogonal

Obviously, we cannot keep people from making mistakes in the general case. So why bother writing lints at all? I don't know about your user base, but if someone gives me a deal where I can get the assurance that I have not made a class of mistakes in exchange for a bit of compile time, I'd be reasonably interested.

I get the impression that you think every safety net worthless if the user has to take even the most simple step to erect it. Is that correct?

Using just unsafe blocks pushes the verification of all of them to the programmer, while a lint just means they need to remember to load the lint (which only needs to happen once per crate). In other words, lints scale much better.

If you're resorting to a custom Rust dialect should you not go all the way, put unimplemented!() everywhere and require a compiler plugin for this to work, instead of an optional lint?

2 Likes

We could certainly do that in the long run, but I first want to get the API right. It may well be we don't need a custom Rust dialect after all.

Can't you get what you want with a Cell-esque API? This won't allow one to read from a Wom, unless one happens to own it (naturally, the owner of the Wom is considered part of the trusted base, since it technically does need to be read from by some part of the system).

The problem with this is that it reduces the applicability to writing the whole value, whereas I also want to cover cases where indexing is relevant.

That's fixable.

That's currently exactly what glium does.

The problem comes when you have something like this:

struct BufferContent {
    a: [f32; 1024],
    b: [(i32, i32); 2048],
}

Note that the original post was meant to be a wishlist for the Rust language, not a list of problems that lack solutions. When I said that I wanted write-only pointers, it's not because there's something I can't do without them. It's because it's the cleanest solution to the problem.

1 Like