A Proposal for an asynchronous Rust GUI framework
notgull.github.io> Here’s one of this year’s dozen new Rust GUIs.
Right. On the game engine side, there's the comment that Rust has 50 game engines and 5 games. A new GUI framework might not be what's needed.
I'd like to see some of the most used ones get finished. I'm waiting for the six-month WGPU overhaul to finish to unblock the Rend3 overhaul so I can use a newer version of egui. Egui botches layout if there's a line wrap in a scrollable window, for example. Basic stuff like that is broken.
> On the game engine side, there's the comment that Rust has 50 game engines and 5 games.
Bingo.
The problem in the Rust ecosystem is that people are writing libraries instead of applications.
This is backwards. Until you have a couple of applications written, you have zero idea what to abstract.
I'll go further. The Rust GUI ecosystem is fundamentally doomed because they are absolutely insisting that any GUI thing must run on desktop, mobile, and web simultaneously. The abstractions required between those domains are fundamentally incompatible.
Desktop apps want "Give me all your cores. Now." Mobile is all about "Please, sir, can I have some battery?" Web is all about "Back in my day all we had was one damn thread so that's all you get and you'll like it."
I would personally cheer if we had one good Rust GUI framework for each of those cases.
My understanding is that most of the new generation of GUI frameworks/application runtime are targeting all platforms. And there's clearly a demand for such feature set if the popularity of Electron says anything. Software businesses no longer want to specialize on specific platforms. What's more, it's provably[0] doable[1] to[2] do[3] so[4]. So, why shouldn't they?
[0]: https://avaloniaui.net/ [1]: https:/flutter.dev [2]: https://platform.uno/ [3]: https://unity.com/ [4]: https://www.jetbrains.com/lp/compose-desktop/> most of the new generation of GUI frameworks/application runtime are targeting all platforms
And they all fail miserably in innumerable ways that make you reach for OS-specific solutions if those frameworks allow that.
Just the simple fact that mist mobile is touch-oriented small screens with imprecise controls, and desktop is mouse-and-keybord-oriented with big screens and precise controls make the two largely incompatible.
> Desktop apps want "Give me all your cores. Now." Mobile is all about "Please, sir, can I have some battery?" Web is all about "Back in my day all we had was one damn thread so that's all you get and you'll like it."
As a Rend3/Wgpu user, I'm very aware of that.
Web is very "you only have one thread and you will use async."
> The problem in the Rust ecosystem is that people are writing libraries instead of applications.
I totally second this.
Got the same feeling after developing https://glicol.org/
Rust community seems altruistic, so perhaps they're just making the mistake of trying to fix everybody's problems instead of just focusing on the simple immediate one: making money.
I could suggest that this is why capitalism works as a tool to improve society, but perhaps that extrapolates a bit too wildly.
The most popular game engine ever is developed by a company that doesn't make games.
The company's root are of a gamedev (Over the Edge Entertainment) and recycled their in-house engine as a product. Godot Engine has similar roots (look up OKAM Studio)
> The most popular game engine ever is developed by a company that doesn't make games.
So? They have the feedback from thousands of games and game developers so they focus on alleviating the pain points.
If you don't have feedback, you may as well not bother.
Ship your Rust game, then you know what the pain points are. Until you ship, you're guessing.
Mostly because it is the only available option if one wants C# instead of C++ with great tooling, asset store and official backing from platform owners.
None of Rust libraries is even close of getting there, even the studios that apparently support Rust, are shipping on Unreal.
So basically, the same problem any cross-platform framework has.
I think a bigger issue is that Rust just isn't a good language to write a game engine or a UI in. I wrote a toy game engine in Rust and quickly learned it basically sucks. The entire reason games and UIs exist is to mutate state in weird, complex, and often circular, ways. Rust's borrow checker doesn't lend itself well to this problem.
> I think a bigger issue is that Rust just isn't a good language to write a game engine or a UI in.
Here's my hot take of the day, as a Rust dev/fan/proponent who's done a few different GUI projects in the language: it's actually mostly fine, but Rust developers won't just settle for "just make it work".
There is true value in a cross-platform common wrapper of native widgets that nobody is really hitting. So many of these GUI attempts are scenarios where it feels like someone stumbled into "how do I build a GUI" and got fascinated by the problem, iterating until they hit a "good enough" point. The problem is that that level of "good enough" isn't actually good enough for a general purpose framework.
Arc/Rc the hell out of it, stop trying to get cute with the borrow checker and lifetime handling, and just make it work. 99% of applications that ship today don't need your novel approach to some virtual DOM diffing algorithm, they just need to reliably shit stuff out on a screen in a way that interacts cross-platform well enough.
> Arc/Rc the hell out of it...
Ooof, and now were back to what's a fundamental problem in a lot of "traditional" C++ game code bases I've seen (including my own stuff I wrote in the late 90's to early 2010's).
Refcounting overhead cannot be ignored when performance matters, and since refcounting usually 'infects' the entire code base, it's impossible to fix once it becomes a performance problem - because then it's too late since it would mean rewriting everything from scratch with a new approach to lifetime management.
> what's a fundamental problem in a lot of "traditional" C++ game code bases
I'll be more clear that I consider the needs of games and the needs of general apps to be very different, and I am particularly discussing the latter and not the former.
> Refcounting overhead cannot be ignored when performance matters,
People throw around the meme of "performance matters" in the GUI space when we've had mostly working solutions for the 99% percentile of applications for decades now. You're going to throw pretty much all your work to a background thread and send some updates over, or you're going to draw to a canvas of sorts that will likely bypass things altogether.
Refcounting is fine for a GUI framework and I am arguing that there is more value in something that ships and works today than waiting around for the next research project GUI framework approach to take off.
Note that I am not saying there's no value in the exploration of a GUI framework that feels "right" for Rust, I'm just saying that the insistence on that being the only goal is odd and ultimately holding the community back.
If you treat every UI widget as its own refcounted object (or even multiple), maybe even uniquely heap-allocated, arranged in deep hierarchies, and share references to those objects within and outside the UI code, then the situation isn't much different from a typical OOP game code base (just replace "UI widget" with "game object")
The most important 'technical quality' of both application types is that user interaction latency must be low, and everything animates smoothly with the display refresh rate without hickups.
Refcounting overhead isn't much of a problem if the number of refcounted objects is in the low hundreds or low thousands (e.g. a simple game or UI application), but it will become one with tens- or hundreds-of-thousands of refcounted objects which are frequently created, destroyed or "shared" (and with a naive approach those numbers are easily in reach as soon as UI table views come into play) - one typical problem for instance is destroying an object at the root of a large dependency tree, which then may 'ripple outward' and cause the destruction of thousands of other objects, causing noticeable hickups (not much different from garbage collection spikes).
> but it will become one with tens- or hundreds-of-thousands of refcounted objects which are frequently created and destroyed (and with a naive approach those numbers are easily in reach as soon as table views come into play)
Look, I'm sorry but it's just bananas to throw around "hundreds of thousands of refcounted objects" when I've already bluntly pointed out that I'm discussing general application building. Joe Schmoe wanting to put together an email client should be able to slap a column view somewhere that can render a virtualized list and move on with their life.
This has been fine in multiple different native widget kits for some time now. It is not as intensive as what some games want to do UI-wise. The current situation does lead to overly complex UI approaches and contributes to the general approach of "smack it with a web browser" that we all collectively bemoan.
> Joe Schmoe wanting to put together an email client ... and move on with their life.
Tbh, for that type of problem a smart Joe Schmoe wouldn't pick a tech stack like Rust in the first place, and instead just write a simple web app (because implementation details aside, the next problem is how to distribute the damn thing without "sideloading" or "potentially harmful download" warnings popping up all over the place).
You're focusing on the "email" part when the point is the widget type itself, which signals to me we've reached some form of an end to this I guess.
(And that's not even going in to why one might not want to deal with the web)
I agree refcounting is fine in state in a GUI, i.e. for using the GUI framework. But IMHO it's not fine for in state in many games.
It might also be less fine so for writing a GUI framework/library.
But it doesn't need to be IMHO as for writing the framework/library you can put your hand in the box of fancy data structures and use what's best suited even if it's implementation involves unsafe code as long as it's contained, well tested and fuzzed.
Yet, not only does the number one C++ engine use refcounting, plenty of commercial engines now have C++ relegated to the core engine, with a scripting language using some form of automatic memory management on top, that drives most of the game code.
Despite all the fancy rendering-tech features, Unreal Engine's "core design ideas" are still deeply rooted in the late 90s and early 2000's (just as Unity's, CryEngine/Lumberyard/O3DE's or whatever it is called nowadays - and most of the other big engines that survived from that era). That's also exactly what I wrote above, refcounting is usually entangled so deeply into those engine designs that it can't be fixed without a complete rewrite (Unity attempted it with their new ECS system, but I have no idea how successful that was, AFAIK it still feels like bolted on to the side).
yes, except thats why you use stuff like ECS systems
which involve fundamental data structures so writing them might involve unsafe rust code
but using them doesn't and they work as well in rust as outside
similar resource/allocation pools in combination with handles into them are a common practice to improve performance, and they work well in rust too (through like e.g. an ECS they often are unsafe to write but safe to use)
So while I would agree that "Arc the hell out" doesn't work for game engines (but IMHO does work for GUIs), I also would argue that if you use common patterns you anyway might have used anyway for better performance, complexity handling and/or reduction of the consequences of buggy code (e.g. prevent crashes) you don't have a problem with using rust.
Through I guess that makes writing "toy" games where you normally wouldn't bother with such patterns a much less nice experience to write in rust.
At that point, why Rust? Like, ARC has a much higher overhead than a good GC.
There are a litany of benefits to Rust that include, but are not limited to:
- Cargo and the general ecosystem
- The relative ease of integrating with just about any other system
- The ability to just build a binary rather than dealing with bundling interpreted languages (like with general Python/Ruby/JS/etc)
See my other comment in this thread for why I consider that "higher overhead" to be meaningless for general applications.
> There are a litany of benefits to Rust that include, but are not limited to
Other than Cargo, none of the advantages you mentioned are actually advantages, and are just basic features that are shared by multiple programming languages for decades.
If it's hard to justify Rust's use, why insist it's a decent tech stack for this sort of applications in spite of all of the evidence to the contrary?
I thought I made it clear - but apparently I should have been far more blunt - that I don't particularly value going in to a comment chain regarding "justify Rust's use for $X". I say this to point out that you're trying to imply:
> If it's hard to justify Rust's use
I don't consider it hard, I'm just not spending time on it because it's not worth it to me. If I felt it was worth it, I could discuss language features or so on (error handling, functional programming idioms, etc). It's covered fine elsewhere. ;P
> Why insist it's a decent tech stack for this sort of applications in spite of all of the evidence to the contrary?
This entire thread is due to my not buying the "evidence" that it's hard.
Go write in whatever language you want. I want to write in Rust, and I want a functioning GUI framework and I think people are overthinking it. shrug
Your empty claims hold no water. You stat you don't consider it hard to justify Rust's use, but you still failed to provide any suport, let alone a coherent argument, justifying it's use.
I get fanboys want to support their pet tech stack no matter what and in spite of all evidence, but hand waving over the problem doesn't lead to progress. It just pushes an irrational belief that helps no one, and just showcases a need to lose touch with reality.
It depends on the use-case. Also with Rust, unlike Swift for instance, you only put behind an Arc what needs to be, whereas in a managed language you pay the GC overhead for the entirety of your objects.
ObjC's and Swift's ARC (Automatic Reference Counting) is not just dumb "reference-count everything", the compiler does static lifetime analysis and removes redundant refcounting operations.
In theory at least this may actually yield better results than in C++ and Rust where refcounting is implemented as stdlib feature and optimizations rely on "zero cost abstractions" late in the compilation process.
But even ARC needs a lot of handholding and manual tweaking if performance matters.
Granted, it probably has improved a lot since 2016, but I didn't have an occasion to update my knowledge about Swift since then. Back then you definitely ended up with tons of pointless ARCs, but in fairness the compiler was brand new at that point so no wonder it wasn't good at optimizing stuff away.
Rust is often the best language for the meat of your program; it's nice to use it for the gravy too.
Many, if not most, programs are not primarily UI's. They exist to control something, calculate something, transform something, et cetera.
All of these need a human interface. Perhaps a command line and a config file is sufficient, but often an interactive UI helps.
You could create an API to your rust program and then write the UI in something else, but there are huge benefits in writing both in the same language.
> Arc/Rc the hell out of it
The pattern I keep needing is single ownership with back pointers. That can be done with Rc and Weak, which pushes the checking to run time. I think it might be possible to do this statically with some extensions to the borrow checker. It's not keeping the forward and back links consistent that is hard. It's making sure that all code sections either have read access to owner and owned, or write access to owner or owned. That's a static analysis kind of problem.
Have you submitted a proposal to the Rust team about this? It would be interesting to read.
I think Rust is a great language to write a concurrent stable ECS in, which doesn’t need circular data structures at all. Like bevy.
Bevy absolutely allows circular data structures and I’d be surprised if any complex game ships without any - Entity references can easily be circular. Most ECS frameworks are their own memory manager which, sure, prevents running afoul of the borrow checker at compile time but not in spirit.
Dangling pointers, null references, and duplicate mutable references all creep into an ECS. They just have new names since it’s all hidden behind Entity rather than a reference or pointer.
In-game structures can of course be circular, logically speaking, by constructing a chain of entities and components that circularly reference each other by entity ID.
But you're saying that Bevy has classic pointer-backed reference cycles? That's news to me.
Its Entity is a pointer by a different name - in that it comes with all the classic problems of pointers. They just happen to live inside Bevy’s memory manager.
An entity has a generational index though which gets around the whole ABA problem of pointers.
The fact is to ship a game you often need to reach into random parts of the heap to get things done. Can you re-architect your game when you hit this wall to get around it? Yes. Can you try to use hashmaps for everything instead? Yes, to a point. But the person writing their game in C++ will laugh maniacally, do the crazy thing, and ship it.
Why is better for this than other languages ? The issue is that it forces you to apply a specific paradigm that may or may not be suited for your use case, and makes it extremely hard to apply other paradigms that you also may need. It's true that there are a few examples of semi-successful games or engines (Veloren, Bevy), but so far it' quite underwhelming... I don't know if the strengths of Rust are even worth it for game dev as a whole
Or Rust is just an extremely difficult language to learn when all you really need are shaders and as you say, mutable state.
Rust will let you mutate state just like C++, but it's difficult to master in a timely way. So, when your time is better spent on the actual shaders and game logic, you have little patience to learn enough Rust to effectively use it.
Exactly. I wanted to code an emulator and said "why not rust". Then I met the BC and pulled hairs for 3 months. After that "honeymoon" with rust, I got to think the b.c. way and my life became just much better. No regret so far. And I even use egui which is very nice to work with.
Until I have to share a linked list between different things... But that's another story.
> Then I met the BC and pulled hairs for 3 months. After that "honeymoon" with rust, I got to think the b.c. way and my life became just much better. No regret so far.
I’m sorry but it sounds terribly like a textbook example of Stockholm syndrome.
I hope one day a language will emerge that has a borrow checker or some other equally effective memory safety measure that doesn’t make the learning curve a fucking smooth upright wall or make its users feel abused and clueless for months.
I think this will come (and already does) from the libraries. They abstract a lot of the hard-to-get-right details for you.
For the rest, I guess the BC comes out of a limitation of the language to express things correctly only. So to remove the BC one would have to build an entirely different language (maybe purely functional ?)
The problem is that you hardly arrive at the correct solution the first time, game development (in the higher game logic layers at least) is an inherently incremental and chaotic process, while Rust heavily prefers careful upfront planning and despises the sort of 'creative chaos' that makes a game work.
it sounds like rust is harder to learn then shaders which is kinda funny
while the base concepts of shaders might be easy, IMHO are far far harder to use correctly and maintainable then rust
anyway I agree that for a lot of use cases you want a ready to go game engine where most of the code you write is in some form of scripting engine on top and only the perf relevant code is in whatever native language you use
through many people today start programming without knowing C++ and I would argue rust is much easier to learn then C++ with that one small problem that C++ can give you easily an illusion of having a proper understanding of it while still missing very essential parts which can not only lead to failing but also RCE security vulnerable code
I would disagree with this.
Some points:
- UI frameworks and Game Engine tends to be huge when completed to a point where they are an easy choice for anything beyond some fun projects. The _huge_ majority of them have organizations/money backing in some form, through sometimes implicit. So the huge majority of open source versions of them no matter which language they are in will never be competitive.
- the state of established UI frameworks is kinda a mess, the divergence between the research world and what is practically used is in most places quite often humongous (e.g. what the blog post describes is conceptually a version of using continuations for GUI libraries and state handling in them, something well established as "a grate idea, if you can make it work nicely with the limitation of widely used programming languages" in computer since since I think 10+ years or so), and it's pretty common that patterns people are used to (e.g. inheritance for code reuse) are actually anti-patterns which kinda somewhat happens to be usable in that case but have serious issues you IMHO often don't notice until using them in larger projects (as in many people working on the software in parallel) where it's then often blamed on other things
- due the the mess the state handling in GUIs often ends up in it tends to profit from using a GC and the using borrows only in rust will likely fail. But then given the perf-characteristics and what tends to be a hot paths of GUIs _you really don't need to write borrows only code_, using an Rc/Arc and similar tends to be perfectly fine, i.e. the issue is in my experience more people obsessing over micro optimizations for that context
- on the other hand for game engine you often want to avoid GC for the engine/core parts themself (i.e. not dialog script and similar) and in general have a much stricter state handling using stuff like a ECS, there is no reason for rust to not handle that well. Through writing the ECS library might involve usafe code, due to it involving writing fundamental data structures. Most importantly in recent years "memory safety" of C++ has become an increasingly bigger issues for AAA-games, especially for multiplayer games where there is a risk of a hacking the users computer through the game client, and the money spend on combating that issue is non trivial to a point that you probably should avoid most code which conceptually works in C++ but couldn't work in rust due to the borrow checker (assuming you already have a well done ECS library with the right soundly implemented data structures).
- similar to writing fundamental data structures, writing a game engine proper is really really hard but seems simple as long as it's only a "toy", rust isn't good at that, sure. But also there is limited value at being good at writing throw away toys which can't be used in any production context IMHO. What does matter is if you can have a nicely usable production ready _maintainable_ game engine in rust, and AFIK yes you can somewhat already have.
Perhaps experimentation is a sensible thing in the early days of the Rust ecosystems
My kind of unscientific impression is Java has a lot of libraries with a relatively few applications.
While c# is a lot of applications with few libraries
I've just wanted right click menus and second screens on Wayland for ages.
My herd of yaks is only getting higher over time...
A classic use case for coroutines is easily turning a push (callback) interface into a pull (return) interface. I'm sure many people have thought of using coroutines to handle GUI events this way. Certainly I have, and have even written some code along these lines in a macOS app primarily written in Lua (Yue as the GUI toolkit, along with my Lua I/O event library, cqueues, which I used to help tie into some event sources for which Yue lacked support; cqueues was designed to integrate with, rather than displace, other event loops).
However, GUI frameworks are a nightmare of criss-crossing events and state, and in practice callbacks are the least of your worries. In other domains callback interfaces often force you to scatter what would otherwise be highly localized logic, but it's the nature of GUI frameworks that your event logic will tend to be short, chunked, and scattered regardless, which is demonstrated by the toy examples. Yes, the async pattern turned event pushes into event pulls syntactically, but that's it--there was no real payoff.
I'm sure somebody will eventually go the distance with this approach. I won't dispute that the exercise would be a fun ride.
I'm very interested in seeing if using the commonly implemented forms of compiler support for async programming can also be well used for GUI programming. One wishawa[0] is also perusing this approach in Rust but I first came upon this idea from the crank-js[1] authors. It wasn't clear to me why that one never went anywhere. Was it failure with the approach or was React just a good solution in the space? I can say this though, there's something strikingly elegant about those initial samples of using JavaScript generators for components.
Thanks for reminding me about Crank; it was The Hot New Thing for about a week when it launched. I’m guessing the project lead just couldn’t convert that initial interest into useful contribution. Glad to see he’s still working on it, though.
I suggest all GUI developers to familiarize themselves with the cutting-edge frameworks in the frontend community before proposing a new framework. This way, they can avoid the hassle of "reinventing x".
Regarding this proposal, it wouldn't hurt to take a look at the implementations of `preact signal` and `vue3 ref`
In addition, the overly strict memory management of Rust is a disaster for GUI programming. GUI states are already complex enough, writing GUI in Rust is like adding an enemy called "compiler" to oneself.