Giving up on wlroots-rs (2019)
way-cooler.org
way-cooler.org
"Way Cooler Postmortem"
http://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem...
It’s really not. That is a fringe benefit; there are a dozen other reasons to use Rust over, say, C++. The syntax is cleaner, there are async await semantics, locking is way better, destructors aren’t a dumpster fire, macros aren’t the wild west, there just isn’t as much cruft... and et cetera.
So they finally sorted out the remaining issues with Drop traits and everything macro related works on stable?
No, the roots, the general unkempt hacked-together style had started by then already :( it's the efforts of last decade or so which made a relatively reasonable language out of C++. It wasn't corrupted by the industry, but it did corrupt the industry to some extent.
C++ helped me avoid C, because I could drop down to the C subset when obliged to do so, and still take advantage of C++'s improved type safety, just validating at the end that a C compiler would also be happy with the code, and thanks to its Bell Labs heritage it has always been a first class language across all major UNIX/POSIX platforms.
I know. It's hard to miss. :P My point was to reflect your claim of the suitability of C++.
Rust is still young. Much younger in some domains than others. GUI is obviously one of them, and I suspect it will be quite a while before it ever catches up, if at all. If that were my requirement to use Rust, then I'd forget about it and check back in five years.
I'm not sure where you are on the innovation curve[1], but I'd say Rust and GUI are all the way to the left right now. (I don't think Rust itself is, but GUI in Rust is.) There are bindings to existing toolkits, but as you say, they're hard to use. So I think some innovation is required and people are working on it.
My guess is that you're somewhere in the "late majority" group on that curve. Partially because you seem to require support by the platform itself (not just platform support by the language) and also due to requiring a mature solution to a deeply complex problem (GUI). Those things tend to take a really long time to happen, if at all, unless they are built and created by the platform itself.
Your desire for binary libraries isn't as out of reach though I think. It's just a hard problem to solve and there just honestly isn't a ton of motivation to do it as far as I can tell. It might require a well funded player to help move that along. (Not just in the work required to do it, but in the resources required to provide it I think.)
[1] - https://www.ou.edu/deptcomm/dodjcc/groups/99A2/theories.htm
https://eli.thegreenplace.net/2015/c-deleting-destructors-an...
Quoting Stroustrup:
Introducing operator new thus made the use of free store more convenient and less error−prone. This increased its use even further so that the C free store allocation routine malloc() used to implement new became the most common performance bottleneck in real systems. This was no real surprise either; the only problem was what to do about it. Having real programs spend 50% or more of their time in malloc() wasn’t acceptable.I found per− class allocators and deallocators very effective. The fundamental idea is that free store memory usage is dominated by the allocation and deallocation of lots of small objects from very few classes. Take over the allocation of those objects in a separate allocator and you can save both time and space for those objects and also reduce the amount of fragmentation of the general free store.
I think almost no one needs custom new and delete today, not only because they are very clunky and obscure, but especially due to the fact that modern C++ features such as move semantics, allocators, std::optional<> and NRVO make them almost redundant and unnecessary.
It's not just a relic of the past. This was last year.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p137...
> 7.7 Matchers and Extractors: Other Languages
Hmm... that "Other Language" looks quite familiar.
And still no sum types.
I do wonder how they are able to pull it off. It is truly remarkable in some way that they are able to have so many language features without it being detrimental to the language.
Or do you think that Fortran 2018, C17, C# 9, Java 15, Python 3.9, COBOL 2014, Ada 2014,... are any different?
But that's the whole point.
https://www.stroustrup.com/bs_faq.html#multiparadigm
You may prefer using 20 different languages for writing your app, I'd much rather write 20 EDSL embedded I a single host language.
https://pubmed.ncbi.nlm.nih.gov/21875228/
https://www.youtube.com/watch?v=EJ5rx5lDIoc
https://www.amazon.com/Creativity-Constraints-Breakthrough-P...
If you google constraints and creativity you will find an endless sea of research.
Otoh most useful large software is written in C++ - adobe software like Photoshop, Illustrator, Premiere, After Effects, most big music making software (Ableton, Cubase, etc), most big video software (DaVinci Resolve, etc) and they definitely don't seem to be lacking in the "creativity" department.
If your hypothesis was true, wouldn't we be seeing a lot of much more advanced software written in different languages ? Yet there are barely any - in audio there's Fruity Loops written in Delphi but other than that...
FYI, the “et” means “and”. https://en.wikipedia.org/wiki/Et_cetera is good reading.
As an example:
The authors of the text were James, Malik, Susan, Srini, and Ying. ... <some more text goes here> ... This was the primary finding supported by Srini, James, etc.
Above, the usage of 'etc' is to shorten the sentence, by avoiding a second instance of the full list.
A simple translation to English shows that the common usage can leave much up to the interpretation of the reader.
As an example:
I like sweet foods: cherries, apples, grapes, and the rest.
So, do I actually mean fruits? Do I mean all other sweet foods that exist? This sentence leaves a lot of interpretation to the reader, and depends on shared context to have any useful meaning. This usage of 'et cetera' generally means "and more in a sequence made up of items categorically in line with what I've named, which I assume, you, the reader, can infer."
This reply is not meant to critique modern usage. Language is a fluid thing, and the use delineating implication, rather than eliding a fully enumerated list, is perfectly acceptable in the vernacular. I just wanted to share some additional nuance regarding the phrase.
Increasingly I’d say I observe et al. being used for (a) lists of people, and (b) finite, known lists; and etc. being used for lists of unknown length. (You may notice there is some unspecified area in that classification. This is deliberate.) This is a definite shift from the original Latin.
Thinking about it a bit more, there is a strong convention in functional languages to decompose lists into "first" and "rest" (regardless of what your language cares to name the functions that return these concepts). Especially in the case of infinite streams, "etc" seems to align with what you've described as the shifting usage in the term. I doubt CS is driving the shift in meaning. Just an interesting parallel I noticed.
There are coroutines in C++20 now. It has the async/await syntax except it's extensible to any kind of coroutine semantics, not just Futures like in Rust.
> locking is way better
RAII was inspired by C++. For making a mutex own its associated data, that's a simple template class which is a well-known pattern. You're also not forced to use it when it is not a good fit.
> The syntax is cleaner
You got me, I have been trolled.
> You're also not forced to use it when it is not a good fit. But like all language criticisms that contain this sentence, it's better when the language makes it somewhat tedious to go outside of the norm, where the norm is defined as the 99% use case. C++ is the opposite; you have to go out of your way to use the generic, safe features like smart pointers or create copy constructors.
Rust's syntax takes cues from ML which means that things like switch statements (matches), if's, and even return are also expressions, which makes it nicer to refactor and allows you more choices when structuring your code.
>This naive style implementation could work, however it would cause a lot of branching code. Each call to wlroots will require a check to see if the handle has been dropped, even though it almost certainly has not been dropped (it can only be dropped between event callbacks, wlroots/Wayland is callback based). This means there will be unnecessary paths (that are hopefully marked cold) that either panic or return an error.
>In C, you're supposed to listen for a "destroy" event on the output and then in the handler you would delete all your references to the object.
If this wasn't satisfactory for whatever reason, then you could implement it as a weak pointer and check for null every time, but you wouldn't have to.
Would this have been a problem in practice I wonder, premature optimization and all.
I think that lots of C/C++ developers have never thought for long about how you end up having to care about the same things in C and C++ too. In C++, you have to think about exception safety, lifetimes and move semantics when you write your code; you have to know if a variable is moveable, if it is worth it to do so, when and by whom it will be destroyed, etc. If you start writing modern and sane C++, you just realise that lots of the patterns you find yourself using are all things Rust already does automatically enforces for you at the language level.
Meanwhile, often people forget that while C just does not care about what you do with memory, you still _must_ remember that objects still have lifetimes, memory still has ownership; the main difference is that the compiler neither enforces nor checks what you're doing with it. Just like in Rust and C++ you must know if a certain resource is yours or not, and you need to be actively aware of it and manage it properly by clearly documenting how it should be managed and where you expect it to change ownership.
I cannot count how many times I saw C libraries forcing you to use reference-counted objects on the heap just to get out of the messy they used pointers. That is, lots of people find that some of their code references random objects in unsound places without any regard about ownership, and then they get need to find a way around it; refcounting is often the easiest solution. And don't get me started about people managing objects like this:
struct obj;
struct obj* obj_new();
void obj_delete(struct obj*);
that is, simply forcing you to use heap allocation using malloc() (rarely, the objects might come from a pool, but it's not that frequent) without considering that you're forcing people to allocate memory (and fragmenting the heap) for short-lived objects that could have well resided on the stack; it also makes a library unusable for those people who might want to use your library on a system that has no heap or has an heterogeneous memory model where not all memory is the same.I have seen a handful C libraries actually care about allocators, but they are definitely not the norm. This is also the reason why I started using C++ on embedded systems such as the ESP32, where flash is relatively abundant (it gets up to 16 MB in some configurations) but internal, on chip ram is still scarce for lots of advanced use cases (~512K) and you need to rely on SPI-connected RAM to actually get work done. C++ libraries that use allocators can be used on such systems without any issue, by simply writing your own allocator; Rust still has not such a nice allocator system in place, but at least it allows you to use the stack and return almost everything by value.
In the end, my point of view is that switching from C or C++ to Rust should not feel like you're radically changing the way you code regarding ownership and memory management, because it's orthogonal to the language; it's something you that always needs to be a crucial point in the code you design. In my experience I learnt that 99% of the time when you feel that Rust is hindering your design it's because your design is either unsound or depends on trusting safety assumptions that are enforced outside of your program (i.e. the OS is guaranteeing this, or this is not physically possible).
Rust will always guarantee safety as long as the postulates it lies on hold, but you should not expect it to make a potentially unsafe C library 100% sound because the language can only go as far ahead as ensuring the code you wrote in it is doing exactly what you've said it should do, and nothing more.
Lots of C libraries are often based on an assumption of trust. They just assume that some things will always happen in a certain way, or that certain events are not likely o serious enough to justify wasting time on writing code to check them. This is, I think, the reason some things can't be directly model on top of Rust, because Rust has been made explicitly to force you to eliminate the occurrence of certain errors, so you either have to implement yourself boilerplate code to guarantee your library behaves like Rust wants it to behave, or you accept the need of writing unsafe code.
Until you try to do GUIs, or imagine how something like a GUI designer would work, and then the fun starts.
Unless there is some improvements in this area, I don't see Rust replacing the symbiotic relationship that C and C++ enjoy with native GUI toolkits, either on their own or coupled with managed languages.
GUI toolkits are often really old (more than 20 years old at least) and have been either hastily hacked in C (often in C89, like Win32 and GTK+, which is famous to abuse refcounting and C macros and is a prime example of what I was mentioning above as a poorly designed library), or have been designed in C++ before C++11, so they largely depend on nasty hacks and practises that are considered obsolete in modern C++ (like Qt).
Other GUI toolkits are designed around modern garbage collected languages (see WinForms, WPF, Swing, ...) so they're not really usable from non-managed languages. Cocoa is in a weird spot, being written in a pretty-much duck-typed language like Objective-C.
My point is that IMHO it's not Rust itself that's not suited for GUIs, but it's the GUI toolkits that have often aged very badly and are out of touch with modern software development. The ugliness of developing UIs with the old frameworks is I think one of the main reasons why the web has taken over, and why others such as Qt have largely moved to a declarative approach with things like QtQuick (i.e. "almost JS") or Apple's SwiftUI.
It's all about keeping development sane, something Microsoft has IMHO canned several times by trying fix the mess they made with Win32, only by making it worse creating a dozen of competing, almost incompatible toolkits. The fact that the newest of them, written since its inception in modern C++ (C++/WinRT) has a nice to use Rust alternative (Rust/WinRT) shows that the problem wasn't the language all along.
I bet just like it happened with C++/CX and C++/WinRT, Microsoft will be the major user, while everyone else outside Redmond will use .NET instead.
Ralf Levien has already posted several blog post how even writing a Rust specific UI toolkit from scratch is not as straightforward as he thought it would be.
https://raphlinus.github.io/rust/druid/2020/09/28/rust-2021....
The Web has taken over because many junior devs don't know any better, and I am really happy that WebAssembly + WebGL is already bringing Flash like tooling back to the Web.
Building a GUI toolkit is a huge task, with many subtasks that are themselves serious efforts (2D drawing, text, reactive architecture, etc). The Druid project has been engaging these problems and coming up with pretty good solutions. Trying to rush the effort (for example, by trying to lash together existing modules) won't be helpful. In coming months, the project will continue focus on trying to deliver one application, the Runebender font editor, rather than trying to be a general purpose GUI toolkit product. We've been growing a really good community and there are lots of opportunities to learn more about GUI technology and help the project along.
I'm a bit biased, perhaps, but I'm extremely optimistic about the prospects for GUI in Rust. But if people are impatient and hoping for a solution they can deploy right now, no, it's not there yet.
Honestly I cannot see how to achieve something like Swift UI or Jetpack Composer, without littering Rust code with explicit reference counting.
I am also considering having visual editor in real time, and the ability to have a widget ecosystem that one just drops into the tool palette and can bind its connections in any way, possibly not considered by the widget author.
I am also looking forward to how Microsoft will eventually sort out Rust/WinRT, which has an easier job, given that COM is by definition RC based, but there is the whole usability story when putting it face to face with what .NET and C++/CX offer out of the box (C++/WinRT after 4 years in development is still catching up with VS tooling for C++/CX from 2012).
SwiftUI and Jetpack Compose are completely different beasts, though they might look similar from the outside. SwiftUI relies heavily on observable objects and basically a shared-mutable-state architecture. I believe that adapting it into Rust would be a nightmare.
Jetpack Compose, on the other hand, lets the app logic build the UI with method calls that resemble immediate mode GUI (though there are of course important differences, as it is building a retained widget tree). These updates are mediated through a mutable context object ("Composer" in the Jetpack Compose world), which adapts quite nicely into the Rust world. In particular, it does not depend on shared mutable references.
I strongly believe that it is possible to express UI in Rust quite naturally and expressively. I've written a fair amount about this recently and have provided evidence in the form of the existing Druid work and the Crochet research prototype, which represents an evolution of its reactive architecture to better support dynamic reconfiguration (a current weak spot in Druid). Of course, we won't know for sure til we get there, but in the meantime it would be nice if the naysayers at least made informed criticism.
These things trade away some runtime efficiency for developer productivity. And for many applications, that's the right trade. That's not really what Rust is about, though. I think .NET will remain a better choice for that kind of application. I just hope .NET gets a good full-AOT solution (including whole-program optimization) for desktop apps, so users don't have to pay excessively for that tradeoff.
At a potentially high cost in accessibility though.
On the positive note, eventually such frameworks might offer the necessary support.
The post was about writing a Rust wrapper for Wayland -- I thought Wayland was supposed to be a relatively new/modern framework.
It seems like windowing toolkits in general might present some difficult scenarios for Rust, regardless of age.
But as soon as you get down to needing to do your own drawing, a complexity cliff rises up.
The web is the other way around. With sufficient styling you can get a UI which matches your vision without having to write a lot of imperative code, but if you want to put a simple application together quickly, there's a real risk of analysis paralysis owing to how much control you have.
E.g. Delphi's VCL has a simple and clear ownership mechanism. Components are owned by their Owner, which is either the Application (forms), a form (for almost all the controls on the form), or a control (for custom controls - normally nested controls are owned by the form). And there's a couple of eventing mechanisms to resolve peer reference relationships, and to negotiate dynamic control addition and removal. Unless you start adding components to collections or otherwise capturing long-term references to them (and you can use name-based access if you need a safe indirection), leaks and dangling references aren't that common.
Implementing that model in Rust, with usable borrows that stop you creating dangling references, isn't trivially obvious to me, but there are ways around, I think: message passing communication, passing closures in, or ephemeral trees which proxy access to the real UI tree.
That's because this is by far the easiest way to implement Pimpl and allow for safe bindings to higher level languages at the same time. For libraries aimed at high-level usage patterns there simply is no reason to bother supporting other allocation strategies, especially when the library itself needs to call malloc internally anyway.
Often I do actually see low-level C libraries using this pattern:
struct obj {
/* expose your private fields here */
};
void obj_init(struct obj *);
void obj_cleanup(struct obj *);
Where the calling code can allocate the object however it wants, but this won't work for any libraries that want to maintain ABI compatibility between versions.If the display exists for the duration of a callback then I would think simply passing the display as a borrowed reference to the callback would enough.
And if you're gonna use C why not just use unsafe rust?
> This expands out the lines to use the non-proc macro, which in turn becomes the callback hell mess. The only problem with this scheme is now that the control flow is all wrong:
My instinct is that any sufficiently large project is going to hit a problem like this as soon as they try to do something the Rust devs didn't anticipate, unless and until the Rust people get monads working.
Support in stdlib is another question, but can't e.g. Option and Result be used monadically with little effort?
Am I missing something critical?
And if you look at what #[dehandle] is doing, it's blatantly an ad-hoc implementation of do notation for these handle types. It's doing the same thing as async/await, or try!, but in (present-day) Rust everyone implementing a new "context-like" type has to write their own "very complicated procedural macro" to implement a specific variant for that context-like type.
(I don't think you necessarily need do notation to write maintainable monadic code - I'm a big fan of railway-oriented programming style i.e. (Kleisli) arrow composition. But of course you can't do that in Rust either, except by writing ad-hoc implementations of all the operators and helper functions for every different type you want to work with).
However, API usability itself is not the problem. Although it was just a small portion of the blog: "Custom Wayland protocols aren’t possible" actually showed the real roadblock. It means wlroots-rs has no reason to exist.
I don't think he could've used Error, because he needed that custom Handle type to have the right ownership/lifecycle behaviour.
> Although it was just a small portion of the blog: "Custom Wayland protocols aren’t possible" actually showed the real roadblock. It means wlroots-rs has no reason to exist.
But the big concern there is the same problem: an inability to make an API that can be used safely (without massive amounts of custom engineering).