Been thinking about memory management in languages without a GC, and there's an approach I hadn't really seen before that I'm curious how people feel about compared to Rust.
In this model you can allocate objects as much as you like, pass objects around and put them in collections, all without lifetimes or moves. Nothing is ever freed automatically, not even at end of the scope. If you want the memory back you write free obj; yourself, and the compiler has to prove nobody else can still see it. If it can't prove that, it won't compile. Same for use after free and double free.
The Ironwood language does it this way. It's compiled with a closed-world link, so the compiler sees the whole program when it checks those frees. There's also defer free x; for cleanup on exceptions. If you just never call free, the memory stays allocated and you get a warning. Fine for a CLI tool, not so fine for a server.
The appeal is that you only have to prove anything at the point where you free. The rest of the code doesn't care. The downside is you have to remember to free.
Rust gets you automatic drops and a stronger guarantee overall, but you're thinking about ownership on every line, which at least for me is counter-productive.
Which memory management approach would you rather work with? And why?
```java
// Ironwood: does NOT compile
public class Garage {
private static class Engine {
void start() {
System.out.println("Vroom!");
}
}
private static class Car {
private final Engine engine;
Car(Engine engine) {
this.engine = engine; // the car keeps a reference to the engine
}
void drive() {
engine.start();
}
}
public static void main(String[] args) {
Engine engine = new Engine();
Car car = new Car(engine);
// Compiler error: the car still holds a reference to the engine,
// so freeing it here would leave car.engine dangling.
//
// error: cannot free 'engine': allocation is still borrowed by a live wrapper
//
// Fix: free the car first, then the engine.
free engine;
car.drive(); // would use a freed engine
free car;
}
}
```
```rust
// Rust: does NOT compile
struct Engine;
impl Engine {
fn start(&self) {
println!("Vroom!");
}
}
// The car keeps a reference to the engine, so it needs a lifetime parameter.
struct Car<'a> {
engine: &'a Engine,
}
impl<'a> Car<'a> {
fn drive(&self) {
self.engine.start();
}
}
fn main() {
let engine = Engine;
let car = Car { engine: &engine };
// Compiler error: the car still borrows the engine,
// so dropping it here would leave car.engine dangling.
//
// error[E0505]: cannot move out of `engine` because it is borrowed
//
// Fix: remove the drop and let both go out of scope
// (car is dropped before engine automatically).
drop(engine);
car.drive(); // would use a dropped engine
}
```