Why do memory safety features need to be explicitly specified?

Seems to me like C# could behind the scenes track whether or not objects need to be passed into functions and handshake on ownership and borrowing, etc. Why does explicitly declaring it help? Any examples?

The problem is called escape analysis, and as you probably guess, it's hard.

When the compiler sees even a simple function call like foo(bar), it has to know if the foo function keeps the bar (owns) or borrows. Without this information in the function interface, it requires analyzing function body, and it becomes a whole-program analysis problem.

But the deal-breaker is lack of information about the intent. In Rust if you have a Vec<String> and try to do vec.push(&str), the compiler shows you the error right there. C# doesn't know if a vec is supposed to contain owned or borrowed values, so it doesn't know which usage is unintended. If you "blindly" happen to get all ownership and borrowing correctly, then the compiler will be able to see it, and give you efficient single-ownership memory management. But if you won't get it perfect, then the program will have ambiguous ownership, and the compiler won't know if that's a mistake, or maybe you wanted Rc or Arc or Cow like behavior. It's like programming in Rust, but with all borrow checker errors being silent. Instead of a compilation error, you get a slower program without warning.

The problem is similar to why C# requires explicit type information. For example, in JavaScript you can use dynamic types, but if you always use the same functions with the same types, it compiles to faster code. But without type declarations, it's hard to ensure you always get it right, and the compiler can't even tell you if anything is wrong, because from language definition mixing of types is allowed.

So in C# ownership is dynamically typed, and mixing of ownership is allowed, and the compiler doesn't know when to tell you you're mixing it up in ways that are ambiguous and require inefficient runtime checks.

4 Likes

Figured that Rust can detect when it's done incorrectly so therefore possible to analyze. But I guess it can only detect that because it knows that:
A) function signature agrees with calls to it
B) function body agrees with function parameters
C) whether or not variables are allowed to be passed into function.

All that can happen by checking for borrow checker, and ownership syntax.

Which can't be done in C#. Clever.. That makes sense, thanks!

Nitpicking just a little: the compiler may be able to see it. Proving single ownership in the general case is undecidable, so there will always be cases where the compiler "gets it wrong" and has to be conservative. In C# this might mean your program runs a little slower than it could. In Rust by contrast it means you're frustrated for a while until you (a) figure out a way to convince the compiler you're right, (b) agree to relax the guarantee of single ownership by choosing another type, or (c) agree to shoulder the responsibility for proving correctness yourself by using the unsafe escape hatch.

2 Likes

Yeah, unsafe is also important here. It can contain code that is impossible to analyze, or appears to violate ownership. In C# without such escape hatch you wouldn't be able to express special ownership of RefCell, Mutex, etc.

True but in those circumstances, you could probably just handle the memory the way C# currently handles it with garbage collector and what not and I imagine it would still be pretty rare.

But having to track every single variable as it floes through every single function down to the lowest level functions from every single place to determine whether to borrow the memory.. that would be way, way to slow to compile the program. I suppose maybe as a release version could be done... but still seems too crazy to be worth it. Probably talking years of compile time for a large program.