The one Rust would've prevented was a simple undefined memory access: https://github.com/ghostty-org/ghostty/pull/7982 (At least, I'm pretty sure Rust would've caught this). In practice, it meant that we were copying garbage memory on the first rendered frame, but that memory wasn't used or sent anywhere so in practice it was mostly safe. Still, its not correct!
Thanks for validating my assumption that once you introduce a big blob o' C all bets are off and you're back to Valgrind (or similar tooling).
> One argument is that the richer, more proven ecosystem of wrapper libraries may have prevented it
Yeah but where's the fun in that? ;)
Rust's bindings fully embrace GTK's refcounting, so there's no mismatch in memory management.
It is built on top of gtk4-rs, and fairly usable: https://github.com/hackclub/burrow/blob/main/burrow-gtk/src/...
I'm sure the gtk-rs bindings are pretty good, but I do wonder if anyone ran Valgrind on them. When it comes to C interop, Rust feels weirdly less safe just because of the complexity.
> gtk-rs you are pulling in 100+ crate dependencies and who knows what lurks in those?
gtk-rs is a GNOME project. A lot of it is equivalent to .h files, but each file is counted as a separate crate. The level of trust or verification required isn't that different, especially if pulling a bunch of .so files from the same org is uncontroversial.
Cargo keeps eliciting reactions to big numbers of "dependencies", because it gives you itemized lists of everything being used, including build deps. You just don't see as much inner detail when you have equivalent libs pre-built and pre-installed.
Crates are not the same unit as a typical "dependency" in the C ecosystem. Many "dependencies" are split into multiple crates, even when it's one codebase in one repo maintained by one person. Crates are Rust's compilation unit, so kinda like .o files, but not quite comparable either.
A Cargo Workspace would be conceptually closer to a typical small C project/dependency, but Cargo doesn't support publishing Workspaces as a unit, so every subdirectory becomes a separate crate.
As Windows proves, it is more than enough to write production GUI components, and the industry leading 3D games API, since the days of Visual Basic 5.
In fact that is how most new Rust components on Windows by Microsoft have been written, as COM implementations.
EDIT:
I looked it up because I was curious, and a Drop trait is exactly what they do: https://github.com/gtk-rs/gtk-rs-core/blob/b7559d3026ce06838... and as far as I can tell this is manually written, not automatically generated from gir.
So the safety does rely on the human, not the machine.
The Zig version seems to be a fix for one crash in a destructor of a particular window type. It doesn't look like a systemic solution preventing weak refs crashes in general.
> 2. All other memory issues revolved around C API boundaries.
Is this something Rust, or any other language, has the ability to prevent any more than any other language? Or once you introduce a C API boundary is it back to tools like Valgrind?
Maybe you could even say Zig is at an advantage there, because tools like Valgrind are considered part of the game, whereas in Rust they are way less commonly used (pure-Rust codebases don't need anything of the sort, unless you are using `unsafe`).
In addition, it's often considered permissible for C APIs to exhibit undefined behavior if their API contracts are violated. This means that a bug in Rust code that calls into a C API incorrectly can (indirectly) cause undefined behavior. For this reason, all FFI calls are marked unsafe in Rust.
The typical approach to using C libraries from Rust is to create a "safe wrapper" around the unsafe FFI calls that uses Rust's type system and lifetimes to enforce the safety invariants. Of course it's possible to mess up this wrapper and have accidental undefined behavior, but you're much less likely to do so through a safe wrapper than if you use the unsafe FFI calls directly (or use C or Zig) for a couple reasons:
- Writing the safe wrapper forces you to sit down and think about safety invariants instead of glossing over it.
- Once you're done, the compiler will check the safety invariants for you every time you use the API -- no chance of making a mistake or forgetting to read a comment.
[0]: This could be avoided/mitigated with some kind of lightweight in-process sandboxing (e.g. Intel MPK + seccomp) to prevent C libraries from accessing memory that they don't own or performing syscalls they shouldn't. There's some academic research on this (and I experimented with it myself for a masters thesis project), but it generally requires some (minimal) performance overhead and code changes at language boundaries.
Calling it a "safe" wrapper, when that safety is entirely dependent on (a) the correctness of the hand-written wrapper, (b) the safety of the underlying FFI code, has always been a huge stretch of terminology. It's more like a veil of safety, so we can shield our modest eyes from impure code that flaunts its unsafeness.
Rust has no magical ways of turning unsafe code safe, nor is it in any way special in being able to create statically-verifiable abstractions around an unsafe core.
Don't get me wrong, Rust is memory safe when used as a cohesive system, and I would encourage its use as such where memory safety is desired. But the idea of "safely" wrapping unsafe FFIs reminds me of the idea of packaging underwater mortgages into mortgage-backed securities and selling them as a safe investment.
Fully agreed on that -- the unsafe keyword means it's unsafe.
> nor is it in any way special in being able to create statically-verifiable abstractions around an unsafe core.
This isn't really true. Rust's type system has:
- Affine types
- Lifetimes
- Borrowed and exclusive references
- Thread-safety
- Explicit delineation between safe and unsafe code -- with no UB in safe code, and a (cultural) design principle that unsafe code is not allowed to make unchecked assumptions about the behavior of safe code
Most mainstream languages do not have any one of these features, let alone all of them. Thus, Rust's ability to "create statically-verifiable abstractions around an unsafe core" is in practice far more powerful than in C, C++, or Zig. You certainly have the ability to create some such abstractions in other languages, but you cannot statically verify nearly as many properties. (And of course there are languages like SPARK that can verify even more statically than Rust.)
In my experience, as a systems programmer who heavily uses Rust to interact with FFI, hardware MMIO and DMA, interrupt handling, networking code, etc., Rust's ability to safely abstract unsafe primitives is easily the most practically useful aspect of the language. It's not a silver bullet that turns incorrect code into correct code, but it is incredibly good at verifying the correctness of a large application built from small low-level primitives. If you're interfacing with buggy C code, Rust certainly isn't going to help you much -- but it does makes a huge difference in preventing "you're holding it wrong" bugs when interacting with a C API.
An unsafe block declares an axiom: you're asserting to the compiler that you've verified (statically with the type system or dynamically with runtime assertions) all preconditions necessary for some primitive to be sound (free of UB). The compiler can then use that axiom to prove the soundness of all code that interacts with that primitive. When I write a driver to perform a DMA transfer, I only have to think through all the concurrency, alignment, lifetime, moveability, caching, and cancellation requirements once, and the compiler will check them for me every time I (or anyone else) use that driver in an application.
It's much much easier to say that a reference is alive here, and only here can we derefence it, AND every other business code elsewhere.
Maybe they wouldn’t even be able to write gtk bindings in rust