this post was submitted on 17 May 2025
28 points (91.2% liked)

Rust

6938 readers
24 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

[email protected]

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 2 years ago
MODERATORS
you are viewing a single comment's thread
view the rest of the comments
[–] sugar_in_your_tea 12 points 4 days ago (4 children)

TL;DR - I didn't read the whole thing, but I couldn't help but ask about the elephant in the room: Lua (or another fast scripting language).

Here's the workflow:

  1. Write core logic in Rust, and use Lua for everything else
  2. As code stabilizes, port Lua code to Rust
  3. At the end, you have a nice modding API

You get most of the benefits of Rust, hot reloading, etc, precisely what OP was asking for. You can even use Lua code to break the rules in Rust.

A good game development language would let me define the optimization level at the module, or even at the individual function level, and preserve as much debug information as possible for these half-optimized builds.

Yes please!

dreaded orphan rule

Yeah, that always struck me as stupid.

any attempts to make life better for programmers via tools like Casey's DLL trick—the only tool available, really—are met with the knee-jerk reaction of: That's unsafe, sounds cursed, and I don't like it

As long as it's not allowed for "production" builds, I don't see an issue. Why not just have a compiler flag or something that turns it on or off? Then you can be ergonomic in development and principled in production.

The note about WebAssembly is odd, it's just slapping a fresh coat of paint over the same abstraction. If you're communicating with part of your app in WA, congrats, you have a crappier version of the same thing. WA is cool and can be part of the solution, but let's not clutch our pearls here.

the focus should be on value types, not memory allocation

Isn't that what lifetimes are? You're proving to the compiler that this can be a value type given it's known lifetime. If you don't know how long something will live, it has to go on the heap or you can have memory safety issues.

async

I agree and disagree here. On one hand, I do a lot of web dev, so async is really nice in keeping things fast. On the other hand, mixing the two sucks.

Here's my wishlist:

  • split stdlib into three parts - core (nostd) , sync, async
  • build the runtime into the stdlib - it's weird to me that I need a third party lib to use async; let me replace it like I can w/ the memory allocator

borrow checker

Yeah, the borrow checker is a bit too strict IMO. Ideally, the borrow checker would only trigger on things that could be run in parallel, such as with threads or async.

That said, I haven't had many issues, but I also don't make games. Generally speaking, copying is fine, which lets you have one place for doing modifications if you can send modifications to some kind of store.

All that being said, I feel like the real problem here is that OP wants one tool to solve the complete problem. They brush off WebAssembly for some reason. They don't consider writing a portion in a scripting language. They don't consider a microservice architecture, where parts can be hot swapped. Etc.

Rust is great at things it's great at, so use it for those. Build a system that has stable parts in Rust and unstable parts in something easier to iterate on.

I didn't read the whole thing, bit I got pretty far. Maybe I'll finish later, idk.

[–] [email protected] 2 points 3 days ago (1 children)

The orphan rule is not stupid, it is necessary.

[–] sugar_in_your_tea 1 points 2 days ago

My understanding is that it's necessary given the current design of the compiler. That doesn't mean it can't be removed at some point.

It would be really nice to be able to define trait impls wherever, and it should be possible.

[–] [email protected] 10 points 4 days ago (1 children)

dreaded orphan rule

Yeah, that always struck me as stupid.

It is necessary to guarantee consistency in the trait system, otherwise it could lead to memory unsafety. Even relaxing it in cases where overlapping implementations could always be catched is still problematic because the compiler sometimes performs negative reasoning about traits that it know cannot be implemented downstream due to the the orphan rule.

And if you think about it the orphan rule is not worse than what other languages allow. For example C# only allow implementing an interface when defining a type, while the orphan rule also allows you to implement a trait when defining the trait itself. Not to mention being able to implement a trait only when a generic parameter implements another trait.

Yeah, the borrow checker is a bit too strict IMO. Ideally, the borrow checker would only trigger on things that could be run in parallel, such as with threads or async.

You can still trivially violate memory safety without multithreading or concurrency. The article touches on this a bit (they mention e.g. iterator invalidation) but they fail to address all issues.

https://manishearth.github.io/blog/2015/05/17/the-problem-with-shared-mutability/

[–] sugar_in_your_tea 0 points 3 days ago (1 children)

the orphan rule is not worse than what other languages allow

Sure, but that doesn't mean it can't be better.

Surely the compiler could delay optimizations until the entire project is built, no? Then it knows what implementations exist, and the developer could then decide how to deal with that. Perhaps the dev could decorate the trait impl as overriding all others, overriding one specific impl, etc.

The orphan rule feels like throwing the baby out with the bathwater.

You can still trivially violate memory safety without multithreading or concurrency

Sure, and ideally those cases would be accounted for, or at the very least the dev could annotate each use to turn the borrow checker off for each instance, and that could print something at build time and a linter could flag over it. Unsafe blocks aren't feasible for everything here.

A lot of these situations are fine in practice. Give devs the ability to sidestep the rules and take responsibility for the outcome.

[–] [email protected] 4 points 3 days ago (1 children)

Sure, but that doesn't mean it can't be better.

It's surely interesting though that people continuously complain about them and then praise a language whose equivalent feature is much more restrictive!

Surely the compiler could delay optimizations until the entire project is built, no? Then it knows what implementations exist, and the developer could then decide how to deal with that

It's not really about optimizations but rather:

  • when checking some impls for overlap, the compiler assumes that impls that the orphan rules block will never exist. You thus need to either disallow them (which would make the compiler more restrictive for non-application crates!) or a way to track them (which is easier said than done since coherence and trait checking is very complex)

  • when generating code where specialization and/or vtables are involved. This could be delayed until the last crate is compiled, but at the expense of longer compile times and worse incremental compilation performance.

Sure, and ideally those cases would be accounted for,

AFAIK there's nothing yet that can account for them without allocating everything on the heap.

or at the very least the dev could annotate each use to turn the borrow checker off for each instance, and that could print something at build time and a linter could flag over it. Unsafe blocks aren't feasible for everything here.

You want some annotations to break out of the safe subset of the language but aren't unsafe blocks basically that? Or perhaps you want something more ergonomic, at the expense of safety?

[–] sugar_in_your_tea 1 points 3 days ago (1 children)

a way to track them

Yes, that's what I'm suggesting. Injecting some kind of metadata that gets stripped at code gen time would probably work.

worse incremental compilation performance

Would it really be that significant?

without allocating everything on the heap

I'm talking about compile time.

Start with all of the known safe cases (basic types should be fine), then move on to more dubious options (anything that supports iteration). Then allow iterable types but don't allow iterating over a mutable reference. And so on. If it's a priority to loosen up the rules without sacrificing safety, surely some solutions could be found to improve ergonomics.

Or perhaps there could be some annotations to make a reference as "unsafe" or similar instead of just a block. If something is safe in practice but not verifiably safe, there should be a way to communicate that.

You want some annotations to break out of the safe subset of the language

The annotations would indicate that something unsafe is going on, so it's like an unsafe block, but on a reference. That way it's clear that it's not being checked by the borrow checker, but the rest of the application can be checked.

I really liked the idea of an optional, built-in GC w/ pre-1.0 Rust where specific references could be GC'd. If that were a thing in modern Rust (and the GC would only be enabled if there's a GC'd reference included), we could get a lot more ergonomics around things like linked lists.

[–] [email protected] 2 points 3 days ago

I'm talking about compile time.

Start with all of the known safe cases (basic types should be fine), then move on to more dubious options (anything that supports iteration). Then allow iterable types but don't allow iterating over a mutable reference. And so on. If it's a priority to loosen up the rules without sacrificing safety, surely some solutions could be found to improve ergonomics.

If you want guaranteed safety then the borrowing rules are the most flexible as far as we know.

Just to give a couple of examples of how your idea might be flawed, what do you consider "basic types"? Are enums basic types? Then you've got an issue, because you might get a reference to the contents of an enum and then replace the enum with another variant, and suddently you've got a dangling reference. Would you prefer to prevent creating references to the contents of an enum? Then you're more restricting than the borrowing rules.

Allowing iterable types but not iterating over mutable references is not enough for safety unfortunately. The basic example is getting a reference to an element of a Vec and then calling push on the Vec. This seems very innocent, but pushing on a Vec might reallocate its backing buffer, making the previous reference dangling. Again, would you prevent taking references to elements of a Vec? Then again you become much more restricting than the borrowing rules.

I really liked the idea of an optional, built-in GC w/ pre-1.0 Rust where specific references could be GC'd

That was just syntax sugar for Rc/Arc, and you can still use them in today's Rust, albeit with slightly worse ergonomics (no autoclone for example).

[–] [email protected] 1 points 3 days ago (1 children)

Lua has a really good C api. Not sure if there's anything for rust.

[–] sugar_in_your_tea 3 points 3 days ago* (last edited 3 days ago)

mlua looks nice. It also does async for coroutines, which is really nice.

[–] [email protected] 3 points 4 days ago

@sugar_in_your_tea @SorteKanin Tried to script with Rhai. Got bitten by the fact it doesn't support async.