Rust, for all its faults, at least engages with its critics. I've long faulted Rust's use of Result over exceptions, for example, but the maintainers at least acknowledge that other options exist and each has trade-offs. Not Pizlo and his fans.
To be clear, I like Fil-C. It's a practical implementation of something that should have existed a long time ago. And Pizlo is, in fact, a great programmer. It just rubs me the wrong way that he can't be content with having done excellent work --- he has to claim things that his system doesn't provide (like memory safety under data races --- minor fault, but still) and claim that other systems are worse in ways they are not (e.g. with respect to Rust having unsafe blocks).
Is genuine excellence not enough? Why must he persist in claiming a false perfection?
Why?
That said, I've spent too much of my life chasing implicit control flow to accept exceptions. Heck, C++'s "noexcept" is a strong argument against exceptions all by itself.
Exceptions came to be during the 1990's, as the C++ARM to C++98 standardisation process was taking place.
Naturally during the early days C++ compilers lacked exceptions, as CFront was introduced without them.
Then we had the C folks that were migrating to C++.
When C++ compilers finally supported exception, compilers vendors introduced the non standard switch to disable them, so that existing code could still compile with the expected behaviour.
The standard itself does not acknowledge this as allowed.
This feature was naturally misused by the anti-exception folks, same applies to how RTTI came to be.
So noexcept is a way to be able to write code to appease both camps, while surfacing what variant was chosen by the programmer.
From my point of view allowing disabling exceptions in first place was a mistake, those folks should have moved back to C, or fixed their code.
> result types pessimize code more
What does it mean? > They're arguably noisier at the source level too.
But they are more explicit, which means there will be fewer bugs in your code.Yes. This is why unchecked exceptions are annoying. Checked exceptions are a „failed experiment“ anyway.
A result type (or even Go‘s err) with forced error handling meaningfully increases the robustness of a program in my experience.
I haven't written any Java since it has gained those, so I suspect there might be other aspects making it awkward. And it still would lack exhaustiveness checking that e.g. Rust has.
Unchecked exceptions lead to unhandled exceptions at runtime. We've all seen screens with Java programs running with tons of exceptions in the log, or worse, that crash with unhandled exceptions. This is the result of the unchecked exceptions mess, which is why I won't use languages that use them for routine error handling for anything more than trivial programs.
Exception handling is just annoying from a syntax perspective. Also checked Exceptions have the problem that they bubble up types that a different layer shouldn’t even be aware of due to exception chaining (cause of a cause etc), unless you carefully re-throw them, which nobody did.
Lower ceremony errors are just better to deal with.
https://smallcultfollowing.com/babysteps/blog/2024/05/02/unw...
use std::panic::catch_unwind;
fn throws_exception(){
panic!("hello");
}
fn main(){
match catch_unwind(|| {
throws_exception();
println!("Never reached");
}) {
Ok(_) => (),
Err(e) => println!(
"threw an exception: {:?}",
e.downcast_ref::<&'static str>().unwrap())
}
}
I can disable exceptions in rustc, but I can't disable the influence they have on language and library design (as detailed in the link I posted). Exceptions are the main reason you can't temporarily move something out from behind an exclusive reference, or return an error code from a Drop impl. Heck, Rust wouldn't even need destructors if it weren't for exceptions: the compiler could just tell you when you forgot to free something.> I can disable exceptions in rustc, but I can't disable the influence they have on language and library design
It's the other way around. The fact that unwinding can be (and regularly is) trivially disabled is the ultimate motivator of why panics fundamentally cannot be used for error handling, and this is what influences language and library design (ultimately demanding `Result`-based error handling).
> Exceptions are the main reason you can't temporarily move something out from behind an exclusive reference, or return an error code from a Drop impl
No, this is absolutely untrue, and I'm not sure why you think this.
> Heck, Rust wouldn't even need destructors if it weren't for exceptions: the compiler could just tell you when you forgot to free something.
No, this is also untrue, and you appear to misunderstand the purpose of destructors. Feel free to provide code if you would like to try to make a more precise argument against unwinding.
Tomayto Tomahto.
> I have seen many Rust programs in my day, and not a single one has ever used `catch_unwind`
Yeah, cause it sucks. Hence my original point: all downsides and no benefits. Also how people use Rust's exceptions has no bearing on whether or not they are exceptions.
> The fact that unwinding can be (and regularly is) trivially disabled is the ultimate motivator of why panics fundamentally cannot be used for error handling
Unwinding can be disabled in C++, too. They still call them exceptions over there.
> you appear to misunderstand the purpose of destructors.
In a language with exceptions, destructors (or something similar like try-finally) are required to guarantee resource cleanup. In a language without exceptions, it is enough to simply write the cleanup code at scope exit points. Rust's static analysis is strong enough to ensure you do this correctly in the hypothetical world where the language does not have exceptions.
If your insinuation here is that I don't understand why you might still want destructors in the absence of exceptions, then you've missed my point.
> Feel free to provide code if you would like to try to make a more precise argument against unwinding.
I already linked the precise argument against unwinding. It's written by Niko Matsakis, one of the core Rust language designers. It explains both the "temporary move from behind an exclusive reference" point and the "return a value from a drop impl" point (which the article calls "must move").
If considering only "safety", then Fil-C is more safe than any Rust containing unsafe blocks, no? With the usual caveats about whether an abort() is safe.
With Fil-C, the "unsafe blocks" live entirely within the compiler and runtime. With Rust, the unsafe foundation is the Rust compiler and standard library, as well as any unsafe code within your application or dependencies.
So either way you're in the same situation of relying on the correctness of the unsafe code you depend on. But there are two major differences:
- Unsafe code can be written in Rust, instead of inside the compiler. This is much easier to write and to review for correctness.
- People other than Fil are allowed to write unsafe Rust. This is what Fil's point is about, and yes, it allows you to opt-in to increasing your attack surface by trusting unsafe code written by yourself or your dependents. Rust allows the user to choose where they draw the trust boundary, and Fil-C does not.
It's true that Fil is probably better than I am at writing unsafe code. So "Fil-C is safer than Rust" is true in that sense. But Fil-C is certainly not safer than safe Rust. And sure, you can't run all the Rust code in the world if you compile with '--deny unsafe_code'; but Fil-C can't run all the C code in the world either. Unsafe Rust is rarely needed, mostly only if you want to do pointer crimes or FFI, and Fil-C doesn't support a lot of (perfectly legal) pointer crimes and FFI either.
"Blocks" are relevant as they are how you express programs, algorithms, etc., that you need in order to get something done. Lots of data structures in Rust have a little unsafe somewhere. Users will typically depend on some "specialist" crate author to write them, but it exists, and is a necessary part of practical Rust programming. The pool of unsafety is open and by necessity growing.
In Fil-C, no such specialists are needed, and the pool of unsafety is closed and fixed, no matter what programs, algorithms, data structures you use.
This is not a trivial distinction I think. I think it's easy to acknowledge, especially given its (current) performance cost.
One such example,
https://www.microsoft.com/en-us/research/publication/safe-to...
Why might I write unsafe Rust? To do something not particularly fancy. A use case covered by normal safe Fil-C. Why might I write unsafe Fil-C (compiler) code? To improve compiler or runtime performance or add new platforms, etc. Analogous to wanting to work on Rust's IR or borrow checker.
Very different use-cases.
There are roughly two categories of Rust user:
1. People who are using Rust for application development. Most of these users write zero unsafe blocks in their careers. For these users, "memory safety" was probably not a strong reason to pick Rust, because there are a wealth of other memory-safe application development languages out there.
2. People who are using Rust for systems programming (i.e. programming under resource or environment constraints). These users may write unsafe for performance reasons, or to do things like hardware MMIO; and they're using Rust over C/C++ either for security or just because the tooling is nicer.
The first category of user is unlikely to consider C for application development in this decade; they're going to be comparing Rust against Go or Java or Node.js. Fil-C solves the security problem of memory safety, but it does not free the developer from the difficulty of having to manually write memory-safe C code; their program will just crash if they get it wrong.
The second category of user cannot use Fil-C because of its performance overhead, runtime requirements and/or lack of escape hatches for MMIO/FFI.
Where Fil-C does shine is for legacy application software written in C. Here, it's a free lunch: a way to harden the massive amount of existing software without an expensive rewrite. I would love to see distros shipping pizlonated coreutils, ffmpeg, systemd, curl, sudo, postgres, etc., anything that has a big attack surface, but does not need to be memory-unsafe.
This is a problem I care about a lot, and your work here is truly a monumental advancement in the field. Comparisons to Rust sell it short by inviting endless debate on problems largely tangential to Fil-C.
I have articulated the specific ways that Fil-C is better.
And I’ve articulated the specific ways that Rust is better. You’ve enumerated some of those reasons from your perspective, though I disagree on the details.
I like those kinds of conversations. We shouldn’t shy away from them as a community. They help us grow a shared understanding of the tech
P.S. Also fan of your work. Competition is always good.
I don’t think Rust users can be trivialized into the “two kinds” that you list, and if one of the kinds is “app developer” then I bet you there are apps where Fil-C’s value proposition is exactly right, except just the fact that Fil-C is so new and immature. Say you want to ship a native UI. Using GTK from fil-C is fantastic.
And I specifically disagree with:
> it does not free the developer from the difficulty of having to manually write memory-safe C code; their program will just crash if they get it wrong.
When I write new code in Fil-C, I just lean into the GC all the way, which makes programming in C and C++ so much nicer. I don’t ref count, I don’t use smart pointers, I don’t free and I don’t delete. It makes these languages so much nicer!
Also, Fil-C’s guarantee that it will panic on OOB or if a race goes badly means I spend basically zero time debugging memory safety issues. The reliability of the failures totally changes the dev experience for the better.
That’s subjective obviously. Some folks swear by type systems like Rust’s to catch as many issues as possible. That’s just not how I roll.
All of that said - the reason to use rust and not Fil-C is performance and memory usage. Fil-C isn’t there yet, except for maybe a small handful of cases (like BLAKE3 and xzutils, where the overhead is basically zero for some reason … we’ll probably because I did a lot of compiler opts and sometimes you get lucky and they sort of all hit)
Fil-C as currently implemented does not guarantee panics on unsafe accesses due to races. You dodge the problem by using a private definition of safety under data race that permits program executions nobody would expect.
The worst that can happen in a race is that you read or write an object that would have been accessible even in the absence of races.
The thing that makes races hard to debug in C or C++ is memory corruption; that doesn’t happen in Fil-C
> The thing that makes races hard to debug in C or C++ is memory corruption; that doesn’t happen in Fil-C
is just blatantly false. It's almost as easy to corrupt memory in Fil-C as it is in C. Type confusion is still allowed. You can still do out of bounds buffer reads/writes as long as they fall within an allocation, so any intra-object stuff, as you might get in common packet or binary file parsing, is still about as unsafe as it was before.
Sure, GC's are nice which is why so many langs have them, but that removes the deterministic allocation performance which most C/C++ programmers want (and many times need). Why not just use something like Go then? You have a much richer stdlib available out of the box.
Yeah, I like it. I also like not caring about dangling pointers, UAF, and other things, not even because of security. These are just bugs that are not fun to debug.
But I get the appeal of the freedom of C++. Especially if you deal with FFI and low level stuff, Rust either forces you to write safe wrappers (good investment long-term, but not fun to do), or just use lots of unsafe, in which case there's no benefit.
This, and there's also cargo which is convenient.
> When I write new code in Fil-C, I just lean into the GC all the way, which makes programming in C and C++ so much nicer.
Then it's no longer C/C++. More like C++/CLI maybe. And it probably gets tricky with FFI. Anyway, keep it up on making the world a safer place :)
https://x.com/filpizlo/status/2053351119095791995
And "Rust let’s you corrupt memory / Fil-C doesn’t.":
https://x.com/filpizlo/status/2079253367587737841
These are false claims. The following compiles and runs fine with Fil-C, happily corrupting memory in a way that a safer language like Rust would not allow:
#include <stdint.h>
#include <stdio.h>
struct Account {
unsigned char name[8];
uint32_t privileged;
};
void write_name(struct Account *account, size_t index, char value)
{
account->name[index] = value;
}
int main(int argc, char* argv[])
{
struct Account acct = {
.name = "foobar",
.privileged = 42,
};
write_name(&acct, 8, 1);
printf("privileged = %d\n", acct.privileged);
return 0;
}
His claims about Fil-C being safer than Rust seem to hinge on Fil-C's ability to make safe-er an entire application stack, compared to a hypothetical Rust application that links to C dependencies which (currently) can't be compiled with Fil-C. This is true as far as it goes, but it's a far cry from justifying a blanket statement like "Fil-C is safer than Rust".