Hi,
I get confused about the difference between RAII (init
/del
) and context managers (enter
/exit
) in Mojo.
Why do they coexist? When to choose which?
I have had a look at the implementation of file.open()
, and it turns out that FileHandle
acquires/releases in init
/del
(without even managing errors in del
…), but it is designed and advertised to be used as a context manager in with clauses.
Could you please help me understand all that?
Thanks!
Hello,
the struct that implements context manager methods is a type,
and one instance of it is created (__init__
) like this:
# __init__ of the MyStruct instance
with MyStruct() as ptr:
# ptr can be stored in the instance of MyStruct
# __del__ of the MyStruct instance
# (for example, to free the ptr)
But It is possible to create non-movable, non-copyable context managers,
so theses are useful for creating specific api’s
see Errors, error handling, and context managers | Modular
It is also possible for a struct that would like to implement “context managers”
to simply return self.
For example, the open
function returns a FileHandle
:
fn open[...](...) raises -> FileHandle:
# Note: open can raise an error
FileHandle
implements an __enter__
that returns self:
# note: self is not as a mutable reference in this __enter__
fn __enter__(owned self) -> Self:
#self is the FileHandle instance
return self^ #returns it (as value `f` in example below)
Then the instance is live for the whole context duration:
#FileHandle.__init__()^.__enter__()
with open(input_file, "r") as f:
#(`f` is a FileHandle)
content = f.read()
# f^.__del__()
# (FileHandle.__del__)
Hi @rd4com
Thank you for your reply.
My question was rather when to use RAII vs context managers.
Thanks.
Mojo does not have RAII - side effect of ASAP destructor memory model. Always prefer context managers when you’re doing resource management.
What is then the purpose of __del__()
and ArcPointer
?
In some cases it is very convenient to use them (in @value
structs) and forget about your resources.
In the documentation (Explicit Lifetimes), it is said that:
So far, we’ve described how Mojo destroys a value at the point it’s last used, and this works great in almost all situations. However, there are very rare situations in which Mojo simply cannot predict this correctly and will destroy a value that is still referenced through some other means.
For instance, perhaps you’re building a type with a field that carries a pointer to another field. The Mojo compiler won’t be able to reason about the pointer, so it might destroy a field (
obj1
) when that field is technically no longer used, even though another field (obj2
) still holds a pointer to part of it. So, you might need to keepobj1
alive until you can execute some special logic in the destructor or move initializer.
You can force Mojo to keep a value alive up to a certain point by assigning the value to the
_
discard pattern at the point where it’s okay to destroy it.
Is this considered a feature or a bug? Where is memory safety then?
I fear this is early optimization. What is the purpose of being fast when you are not safe? To crash sooner? If you lose the connection to your database because of premature destruction, what is the purpose of being fast then?
I’m not sure what you mean by this is an optimisation. That part of the documentation is merely pointing out that if you happen to carry a pointer to another field (similar to what you’d do in Rust using raw pointers), then it becomes the user’s responsibility to ensure that the pointer doesn’t dangle during destruction (Drop
). This is within the structure—where naturally safe APIs are implemented using unsafe
under the hood.
Mojo can’t properly track the lifetime of some values - like UnsafePointer. If your struct has an unsafepointer you should manually deallocate here or you may get memory leaks.
However, resources aren’t about lifetimes, they’re about ehh… resources. Network connections, file handles, database transactions, locks, etc. These are resources, you shouldn’t manage them using Mojo’s __init__
and __del__
I believe even the rare cases mentioned in this document is outdated as Mojo’s UnsafePointer can now reason about Origin, it should be able to now keep these references alive without resorting to hacks.
However, the language guarantee is that all the safe parts of the language can do proper lifetime tracking and is always safe, sound, and correct. When you use the unsafe parts you have to take responsibility for maintaining correctness and safety. Mojo will try to help you, but it is ultimately your responsibility.
A good example of where a context manager would be wanted vs relying on the normal object lifetime behavior is a lock, since in Mojo an objects __del__
is called on last use.
# This is all made up
var mutex = Mutex(0)
with mutex.lock() as data:
data += 1
# mutex unlocks at end of manager
If that wasn’t used, things get more complicated, especially since mojo ASAP destruction means that the pattern used with RAII / Rust for a mutex, where it’s unlocked on exit of scope, don’t work. The context manager makes it much more clear exactly how long the lock is held.
Outside of that it mesh’s better with what python users would expect. Although I still prefer to not use them for things like FileHandle and just call close etc since I’m used to Rust in that regard.
Thank you for all your answers.
Now I can imagine situations where context managers are nice to use.
But why not use RAII at all? It composes well.
Imagine we want to do unit tests. Are we supposed to write a with
-clause at the beginning of each test? And what if we have dozens of resources to manage? How do you factor out the with
-clause in your code?
Please don’t introduce scope based RAII in any form.
It is a nightmare in rust, if let vs match (if let temporary scope - The Rust Edition Guide), it is not clear where to drop temporaries.
Mutex also suffers from this in rust. Rust looks to introduce a map method for MutexGuard(MutexGuard in std::sync - Rust), which is essentially a worse with statement, and with crippled control flow.
Furthermore scope based RAII also conflicts with control flow features like safe gotos.
I believe the origin model to be completely sound, it isn’t the case that it will be flaky and unsafe. To clarify, there are two sorts of situations where this happens:
-
Incorrect use of unsafe features. Mojo can’t help, they’re unsafe. That said, UnsafePointer (for example) does carry an origin, so this should be relatively rare.
-
Missing compiler support for origins. The origin model is basically done, but nested functions/capture lists are still not perfect.
I think #2 is on track to being fixed in the next month or two. @Steffi would know more
-Chris