Comment by steveklabnik

7 hours ago

I think this is a fine post. But one comment:

> remember that for compilers which emit machine code, like roc and rustc, doing memory-unsafe things is a big part of the job

I don't really think that this is true, in the way that it's written.

I think that for the hot binary patching / code reloading features, yes, that is going to need unsafe. But for regular old "producing an executable" compilation? Emitting machine code isn't the part that requires unsafe. The language's runtime is a more likely site to find unsafe.

> I think that for the hot binary patching / code reloading features, yes, that is going to need unsafe. But for regular old "producing an executable" compilation? Emitting machine code isn't the part that requires unsafe. The language's runtime is a more likely site to find unsafe.

Agreed! Emitting machine code is not unsafe, since it's just writing bytes down - it's only once you execute that machine code that there's potentially unsafety. The reason I said "a big part of the job" is that in practice a lot of compilers both emit machine code and execute it - but you're totally right that it's not a requirement that a compiler do both.

In addition to the examples you gave (hot binary patching/code reloading, language runtime, etc.), others would be things like evaluating userspace code at compile time (e.g. const fn in Rust, or in Roc any expression that could be hoisted to the top level), running tests and inspecting their output to decide what to display to the user, etc.

Those are the types of things I had in mind when I wrote that.

  • I am disappointed you're downvoted, Richard. This is a fine reply, and I hope you know that a minor quibble with a single line in the post doesn't mean that I think it's a bad one overall. (EDIT: a few minutes later, the parent comment is no longer grey.)

    I also think it's a good thing that you wrote the post in general, when I saw it pop up I was like "oh, of course, this post should exist!" I'm surprised I didn't think about it earlier.

    > evaluating userspace code at compile time

    Usually this would be done via an interpreter, so I'm not sure that it really requires unsafe either. If you are literally executing machine code, sure, but const fn in Rust and constexpr in C++ and many other languages do not do that, as it causes a number of problems (for example, cross-compilation).

    • Also a good point! TIL that Rust and C++ use interpreters for const, although of course that wouldn't work for running tests. Then again, in the specific case of Rust I believe rustc only compiles the tests and then something else like Cargo executes them. Of course, as I noted elsewhere, if rustc emits machine code and then cargo immediately executes it, there's the same opportunity for end user memory being corrupted (due to miscompilation) as if rustc and cargo shared a code base.

      By the way, I thought your question was totally reasonable - my first thought reading it was "Oh yeah I wasn't trying to say that writing bytes is unsafe, I definitely should have worded that differently."

      4 replies →

    • > Usually this would be done via an interpreter, so I'm not sure that it really requires unsafe either.

      Well, I personally have written a const-expression evaluator that actually reuses the rest of the compiler: it compiles the expression in the current environment with some specific adjustments to the codegen settings, launches the temporary executable and gathers its output... frankly, it's more hassle than it's worth compared to writing a separate const-expression interpreter. Plus, of course, it also runs slower since most constant expressions are usually pretty trivial.

Many people try to twist the fact memory safe languages have unsafe code blocks to make the pivot that why bother.

It is like someone arguing that since they always bump the head somehow while wearing seatbelts, then they are only a nuisance and should not be used.

  • Because they view "unsafe" as an escape hatch instead of a feature. It's a way to encapsulate dangerous behavior, tightly, with clear postcondiitions. Sometimes it's the only way to do things like interact with inherently unsafe FFI code, or hardware.

    • I adore unsafe, appreciate it as a feature... but it is an escape hatch. One that is sometimes necessary, one that is sometimes not necessary but might still be (ab)used for performance, or initial 1:1 porting of C/C++ code. There are a lot of cases where that escape hatch should probably welded shut though. Fortunately, the Rust ecosystem has tools like `cargo geiger`, and straight out of the box I can also write:

          // src\lib.rs
          #![forbid(unsafe_code)]

I agree that it’s not inherent to emitting machine code but I do think it reflects a different set of priorities.

In extremely high performance code you use different data structures and algorithms and change your approach to memory allocation. TigerBeetle famously does all memory allocation once on startup.

Roc is attempting to make a similar set of trade-offs in their compiler as Zig, so it makes sense that the author finds many shared patterns.

  • > In extremely high performance code you use different data structures and algorithms and change your approach to memory allocation.

    It's worth noting that the reason Rust doesn't include support for custom memory allocation patterns like Zig does has nothing to do with memory safety. It's more of a historical accident that it just wasn't something that was prioritised early in the projects history and is now hard to change.

    • It's also important to note that Rust as a language does not know about the heap at all, it is purely a library concern. This means that "doesn't include support for custom memory allocation patterns" is purely talking about standard library data structures, and if you need a ton of performance, you're probably going to be writing your own anyway.

      It will be nice when the Allocator trait stabilizes so that the ecosystem can coordinate on making this stuff pluggable, but that's not a direct blocker for getting things done if you need to do things today.

  • I do think it reflects different priorities, but one of those differences is that from my perspective, safety and performance are not inherently at odds. Yes, sometimes it is needed, but not as much as some people seem to think. Sometimes, it also means writing code in ways that communicate things to the compiler that you may not think of if you're not used to thinking in this manner.

    A lot of the ways in which the zig compiler works doesn't use pointers, it uses indices. This stuff is easier to write as safe code, not less easy.

    > Roc is attempting to make a similar set of trade-offs in their compiler as Zig, so it makes sense that the author finds many shared patterns.

    I do think that that makes sense, but it also doesn't mean that they have to. I am doing a compiler project that takes a lot of inspiration from Zig (as my language currently inherits some major things from Zig, and I also care a lot about compiler performance) and it's written in Rust, and does not use much unsafe code (outside of the usual suspects of FFI in the runtime, etc).

    • > A lot of the ways in which the zig compiler works doesn't use pointers, it uses indices. This stuff is easier to write as safe code, not less easy.

      I respectfully disagree. This is only true if you view malloc as qualitatively different from a piece of code that gives you an index for a free object in an object pool.

      Provided you don't ask/give back memory from/to the OS, what malloc is doing is giving you an index (pointer) into a pool of bytes, while manipulating an internal bookkeeping structure.

      Use after free is just you using an index after said bookeeping structure has marked that piece of memory as available for something else (and perhaps claimed already).

      If you have an array of Node structs to represent a graph (like the AST in Zig), and use indices to represent references, you have essentially zero protection in Rust that helps you with finding Node-s that have been used for something else etc.

      The 'asking memory from the OS' aspect for malloc doesn't really change the safety of your language compared to this where it matters - if you do use-after-free on a page claimed by the OS, you get a segfault, which immediately tells you there's a problem, which is much better than silent corruption.

      At least with malloc, you get debug allocators, or other features that can help you in this case. If you are careless with indices in an object pool, and overwrite stuff, essentially, it's up to you to figure out what went wrong and you have no tools to help you.

      1 reply →

Yeah that is definitely 1000% wrong. A compiler can do its job with totally abstract data structures. If anything would need to do unsafe stuff in memory, it would probably be a linker.

That line confused me, too. What parts of their compiler require memory-unsafe operations to produce machine code?

  • They are saying that running the compiled code is memory-unsafe when there is a compiler bug, and that’s what developers do next. The memory corruption happens in a different process.

    In this respect, effectively all the compiler should be treated sort of like an unsafe region because it requires extra care to avoid memory corruption bugs.

    • That's not what it says at all. The section we're talking about is for the compiler and emitting machine code

      > we ended up with about 1,200 uses of unsafe

      > remember that for compilers which emit machine code, like roc and rustc, doing memory-unsafe things is a big part of the job

      Anywhere talking about the `unsafe` keyword is within the Rust code.

      1 reply →

I think if you interpret it charitably it means that any bug in the emitted machine code is already a likely memory-unsafe miscompilation if it is ran.

The compiler itself might be perfectly "memory safe" but the generated binary fundamentally is always at risk (besides WebAssembly I suppose).

I'm fully aware of the separation of compiler and binary, and being able to compile untrusted code safely is nice, but a perfectly safe compiler that generates vulnerable binaries isn't that much better.

  • In context that's clearly not what he's saying, the next sentence is this:

    > Zig has more features than Rust for making memory-unsafe code work correctly, and that was the area where we wanted the most help.

    Zig definitely does not have more features for successfully emitting memory-unsafe machine code than Rust does. I can emit memory-unsafe machine code from typescript if I really want to and nothing at all in the language will get in my way. So the sentence quoted above must refer to the idea that the compiler itself needs to be unsafe, which Steve is right is simply untrue.

  • I do think that is a good point, it's just not what the line actually says. But that's why I wasn't saying "zomg this is WRONG!!!!" but instead, trying to point out that there are subtleties here. For people who aren't as deep in the weeds in this subject, I think the details matter. But again, as I said, I like the post, this is just one thing.

    I am also probably in a more pedantic mindset because, well, I'm writing a compiler in Rust, and the words as written do not resonate with me at all.

    > a perfectly safe compiler that generates vulnerable binaries isn't that much better.

    I do think it's much better. Eliminating classes of bugs in one component is a good thing, even if it's not every component. This is a core lesson of Rust! unsafe still exists, but going from "I don't know what is unsafe" to "only this part is unsafe" is a major improvement.

  • The section this came from was talking specifically about usages of `unsafe` in the compiler code.

    It's not about the memory safety of the resulting binary.

Agreed, that’s disturbingly incorrect.

If anything, compilers are perfect models of trees and well formed programs.

  • Maybe in theory. In practise you have thing like super optimisers. You have side effects that the compiler needs to understand etc.

    That said I'm struggling to think of something that would need to be unsafe.