





















This post blog is the second part of the series of ownership in Mojo. Please make sure to check out the first part, What Ownership is Really About: A Mental Model Approach, as we will build on concepts developed there. This post serves as accompanying material for the deep dive on ownership by our CEO, Chris Lattner. Be sure to watch the video as well, which covers how ownership is implemented in Mojo's compiler, providing further insights and technical details.
Understanding how ownership works in Mojo is essential to leveraging its memory management capabilities effectively. Mojo ensures memory safety similar to Rust and efficiency of C/C++ by enforcing strict ownership rules. In this part, we will explore how this is achieved through different kinds of values, ownership modifiers, and lifecycle management.
When Mojo compiler parses the code along with the type checker it outputs three kinds of values, each with distinct ownership and reference behavior:
Intuitively, these values can be defined using the ownership model we built in the previous section as follows (Mojo-pseudocode):
Pseudocode
alias RValue = UniqueRef[T, Lifetime, Ownership = Unique] alias BValue = ImmutableRef[T, Lifetime, Ownership = Immutable] alias LValue = MutableRef[T, Lifetime, Ownership = Mutable]
These values propagate through the expression trees as the result of the Mojo parse. This is crucial for ensuring that the correct ownership and reference semantics are maintained throughout the program.
Next, we will see how ownership can be modified in function arguments.
Mojo’s argument convention in functions are as follows:
The first rule to note is:
Rule 1: owned arguments take RValue on the caller side but are LValue on the callee side.
When a function argument is declared as owned, it takes an RValue on the caller side but becomes an LValue on the callee side. This ensures that the function has exclusive access to the value, preventing other references from accessing or modifying it simultaneously. (The following is valid Mojo code):
Mojo
# a function that takes ownership of String fn take_ownership(owned y: String): y = String("Bye!") # y is an LValue here print(y) fn main(): var x = String("hello, world!") take_ownership(x^) # x^ is passed as RValue # print(x) # This would cause a compile-time error as x's ownership has been transferred
In above, String("hello, world!") uses the constructor __init__ to create the RValue where var owns it and is named x. Then in take_ownership(x^), the x^ is passed as an RValue (caller side) whereas it is received as an LValue in the callee side which is x = String(“Bye!”). The output of the program is Bye! as expected.
For the purpose of completeness, we should mention that Mojo’s dataflow analysis determines where the last use of a variable has occurred and it injects its destructor __del__ eagerly. This is known as As Soon As Possible (ASAP) destructor. However, when transfer operator ^ is used, Mojo disables the call to String.__del__ and is correctly delegated to happen after take_ownership is done printing the new assigned value. For more, please check out the death of a value.
This table depicts how Value kinds behave with function ownership modifiers.

To implement custom behavior for copying and moving values, Mojo provides the __copyinit__ and __moveinit__ dunder methods, respectively. These methods allow you to define how values are copied or moved, ensuring that ownership is correctly transferred without unintended side effects. For more details, please check out the Mojo manual.
A type in Mojo can be:
Values that can be moved through __moveinit__, transferring ownership without copying the underlying data through the transfer operator ^. The transfer operator ^ is used to convert a BValue or LValue into an RValue. This is essential for transferring ownership while maintaining the original value’s integrity.
The following code simply prints when __moveinit__ is called:
Mojo
struct MovableOnly: fn __init__(inout self): ... fn __moveinit__(inout self, owned existing: Self): print("Move is called") fn main(): var a = MovableOnly() # var b = a # compiler error: 'Movable' is not copyable because it has no '__copyinit__' var b = a^
In the above example, var b = a fails to compile because that assignment used the __copyinit__ which there is none here. Executing the code, outputs Move is called.
Values that can be copied through __copyinit__. The following code simply prints when __copyinit__ is called:
Mojo
from benchmark import keep struct CopyableOnly: fn __init__(inout self): ... fn __copyinit__(inout self, existing: Self): print("Copy is called") fn main(): var a = CopyableOnly() var b = a keep(a) # need to `keep` it otherwise, Mojo compiler sees it's unused and optimizes it away
The above var b = a calls the __copyinit__ and running the code confirms Copy is called. Note that we need to use keep(a) to keep the Mojo compiler away from optimizing it away as it was unused.
An important note is that the transfer operator ^ is still usable here and its behavior is delegated to __copyinit__. We can verify it as follows:
Mojo
fn main(): var a = CopyableOnly() var b = a var c = a^ keep(c)
which outputs:
Output
Copy is called Copy is called
This might be surprising at first! In order for transfer operator to actually move, we need to implement __moveinit__.
Values can implement both __copyinit__ and __moveinit__. The following prints when __copyinit__ or __moveinit__ is called.
Mojo
from benchmark import keep struct CopyableMovable: fn __init__(inout self): ... fn __copyinit__(inout self, existing: Self): print("Copy is called") fn __moveinit__(inout self, owned existing: Self): print("Move is called") fn main(): var a = CopyableMovable() var b = a var c = a^ keep(c)
which outputs:
Output
Copy is called Move is called
Given the above examples, we can deduce the following:
Rule 2: owned argument owns the type if the transfer operator ^ is used, otherwise it copies if the type is Copyable.
The Mojo compiler takes any opportunity to optimize memory management. In the above example, even without specifying var c = a^
Mojo
fn main(): var a = CopyableMovable() var b = a var c = a keep(c)
The Mojo compiler sees that var c = a is the last use of a so it optimizes move over copy.
Output
Copy is called Move is called
Therefore, we can deduce the last rule:
Rule 3: Copy to move optimization if the type is Copyable and Movable on last use.
This is particularly beneficial for large or complex data structures, reducing the overhead associated with copying and enhancing performance. For example, in the following, avoiding extra copy is an important optimization that the Mojo compiler can do easily for us.
Mojo
struct Str: var ptr: DTypePointer[DType.uint8] var sz: Int fn __init__(inout self): self.ptr = DTypePointer[DType.uint8]() self.sz = 0 fn __init__(inout self, str: StringLiteral): self.sz = len(str) self.ptr = DTypePointer[DType.uint8].alloc(self.sz) memcpy(self.ptr, str.as_uint8_ptr(), self.sz) fn __copyinit__(inout self, existing: Self): # deep copy self.sz = existing.sz self.ptr = DTypePointer[DType.uint8].alloc(self.sz) for i in range(self.sz): self.ptr.store(i, existing.ptr.load(i)) print("Copy is called") fn __moveinit__(inout self, owned existing: Self): # shallow ptr copy self.ptr = existing.ptr self.sz = existing.sz print("Move is called") fn __del__(owned self): self.ptr.free() fn main(): var s = Str("hello") var t = s var u = s keep(u)
which the expected output is:
Output
Copy is called Move is called
Values that cannot be copied or moved, ensuring that they remain in a fixed memory location. An example of such special immovable types is Atomic which cannot be copied or moved. Such behavior is critical to ensure correctness in multithreaded environments.
In the second part of the ownership series in Mojo, we built on the mental model developed in the first part and provided practical examples to illustrate how ownership works in Mojo. We covered the different kinds of values (BValue, LValue, and RValue) and how they propagate through expressions. We also explained the function argument conventions (borrowed, inout, owned) and demonstrated how these conventions help manage memory safely and efficiently. We concluded with three fundamental rules:
Lastly, we emphasized that the main goals of ownership in Mojo are:
By integrating these principles, Mojo offers a powerful and intuitive framework for memory management, enabling developers to write efficient, safe, and high-performance code. For a deeper understanding of the technical details and implementation of ownership in Mojo, make sure to watch the accompanying video by our CEO, Chris Lattner.
Additional resources:
Report feedback, including issues on our Mojo and MAX GitHub tracker.
Until next time!🔥
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。