Learning Cairo - Ownership (6)
Prerequisites
Install cairo-run.
Ownership
Ownership is an important concept in Cairo/Rust that sets it apart from other languages. The ownership system in Cairo ensures the safety and correctness of compiled code by specifying that a variable can only be used once. The ownership system prevents common operations that can lead to runtime errors, such as illegal memory address references or multiple writes to the same memory address.
Let's take a look at the ownership rules in Cairo:
- Every value in Cairo has an owner.
- Only one owner can exist at any given time.
- When the owner goes out of scope, the value is dropped.
Variable Scope
Scope defines the valid range of an item within a program. For example:
let s = 'hi';
s
is a short string, and its lifetime starts from the moment it is declared until the end of the current scope. The following example demonstrates the scope range of s
:
{
// s is not valid here, it’s not yet declared
let s = 'hi'; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
In other words, there are two important points to note:
- When
s
enters the scope, it is valid. - It remains valid until it goes out of scope.
Scope of Array Types
Array types are relatively complex, and we can use them to illustrate ownership rules. Let's start with a basic usage of an array type:
let mut arr = ArrayTrait::<u128>::new();
arr.append(1);
arr.append(2);
Here, we define a mutable array arr
and then add two elements to it.
Let's consider a simple example:
use array::ArrayTrait;
fn foo(arr: Array<u128>) {}
fn bar(arr: Array<u128>) {}
fn main() {
let mut arr = ArrayTrait::<u128>::new();
foo(arr);
bar(arr);
}
We first define a mutable array arr
and then pass it as an argument to the foo
and bar
functions. According to the programming syntax in other languages, this should work without any issues. However, when we try to compile it, we get an error:
error: Variable was previously moved. Trait has no implementation in context: core::traits::Copy::<core::array::Array::<core::integer::u128>>
--> main.cairo:7:9
let mut arr = ArrayTrait::<u128>::new();
^*****^
The error message indicates that the variable was previously moved and has no implementation of the Copy trait.
This error message reveals the essence of ownership in Cairo. In the above code, when we call foo(arr)
, the ownership of arr
is transferred to the foo
function. Then, when we call bar(arr)
, it fails because arr
has already left the current scope. This explains the error message:
error: Variable was previously moved.
This design in Cairo may seem difficult to understand, but it actually helps us avoid many runtime errors. In the example above, both foo(arr)
and bar(arr)
receive the same array instance. If both functions could be called normally, and if we add a value to the array in foo
and then add another value to the array in bar
, it would result in writing twice to the same memory location, which is not allowed in Cairo. To prevent this situation, when foo
is called, the ownership of arr
is transferred to foo
, and when bar
is called next, since the ownership of arr
has already been transferred, such issues are prevented at the compilation level.
Copy
Trait
The previous error message also contains useful information:
Trait has no implementation in context: core::traits::Copy::<core::array::Array::<core::integer::u128>>
Let's take a look at what the Copy
trait is.
In the example above, because the ownership of arr
is transferred to foo
, calling bar
results in an error. However, if we have a similar business scenario where we need to pass a variable to multiple functions, how do we handle it?
Cairo provides us with a Copy
trait feature. If a type implements the Copy
trait, passing a variable of that type to a function does not transfer the ownership of the value to the called function; instead, it passes a copy of the value. The Copy
trait can be implemented by adding the #[derive(Copy)]
attribute to custom types. However, if a type itself or any of its component types does not implement the Copy
trait, the type cannot be marked as Copy
. It's important to note that arrays and dictionaries cannot be copied.
#[derive(Copy, Drop)]
struct Point {
x: u128,
y: u128,
}
fn main() {
let p1 = Point { x: 5, y: 10 };
foo(p1);
foo(p1);
}
fn foo(p: Point) {
// do something with p
}
In this example, since Point
implements the Copy
trait, when foo
is called, the ownership of p1
is not transferred; only a copy of p1
is passed. Therefore, we can call foo
multiple times. If we remove the Copy
trait, the compilation will result in an error.
Drop
Trait
In the previous example, after Copy
, there is the Drop
identifier. Let's try removing Drop
and compile again. It will result in an error:
error: Variable not dropped. Trait has no implementation in context: core::traits::Drop::<main::main::Point>. Trait has no implementation in context: core::traits::Destruct::<main::main::Point>.
--> main.cairo:8:9
let p1 = Point { x: 5, y: 10 };
The error message indicates that the variable was not dropped.
As mentioned before, variables in Cairo have ownership. When a variable is defined, its ownership belongs to the current scope. If it is passed as a parameter to another function, its ownership is also transferred to that function. If ownership is not transferred to another function, it remains within the current scope. Cairo specifies that a value cannot outlive its scope unless it has been moved previously.
For some built-in types, Cairo automatically calls an implicit drop
function for variables of those types, allowing them to outlive their scope. For custom types, we need to explicitly indicate the Drop
trait by adding #[derive(Drop)]
.
#[derive(Drop)]
struct A {}
fn main() {
A {}; // Now there is no error.
}
Except for dictionaries (Felt252Dict
) and types that contain dictionaries, all other types can derive the Drop
implementation, allowing them to be dropped when they go out of scope. For example, the following code can be compiled successfully:
#[derive(Drop)]
struct A {}
fn main() {
A {}; // No error here
}
Destruct
Trait
As mentioned before, dictionary types cannot derive the Drop
implementation. However, if a dictionary type cannot use Drop
, the Cairo compiler will report an error for the following scenario:
use dict::Felt252DictTrait;
struct A {
dict: Felt252Dict<u128>
}
fn main() {
A { dict: Felt252DictTrait::new() };
}
error: Variable not dropped. Trait has no implementation in context: core::traits::Drop::<main::main::A>. Trait has no implementation in context: core::traits::Destruct::<main::main::A>.
--> main.cairo:8:5
A { dict: Felt252DictTrait::new() };
^*********************************^
Since the type A
contains a dictionary, it cannot be dropped. Fortunately, Cairo provides us with the Destruct
trait, which can provide similar functionality to Drop
:
use dict::Felt252DictTrait;
#[derive(Destruct)]
struct A {
dict: Felt252Dict<u128>
}
fn main() {
A { dict: Felt252DictTrait::new() }; // No error here
}
Using Clone
for Deep Copying Array Data
Cairo provides the clone
method, which helps us deep copy the data of an array. For example:
use array::ArrayTrait;
use clone::Clone;
use array::ArrayTCloneImpl;
fn main() {
let arr1 = ArrayTrait::<u128>::new();
let arr2 = arr1.clone();
}
First, we need to import clone::Clone
and array::ArrayTCloneImpl
. Additionally, you need to add the following runtime parameter:
--available-gas=2000000
Since clone
implementation involves looping, it requires a specified amount of gas.
Ownership and Functions
As we saw earlier, when passing a variable's value to a function, we can either move its ownership or copy its value. For array types, passing them as arguments to functions transfers their ownership. For other types, they each have their own way of handling this, for example:
#[derive(Drop)]
struct MyStruct {}
fn main() {
let my_struct = MyStruct{}; // my_struct comes into scope
takes_ownership(my_struct); // my_struct's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but u128 implements Copy, so it is okay to still
// use x afterward
} // Here, x goes out of scope and is dropped.
fn takes_ownership(some_struct: MyStruct) { // some_struct comes into scope
} // Here, some_struct goes out of scope and `drop` is called.
fn makes_copy(some_uinteger: u128) { // some_uinteger comes into scope
} // Here, some_integer goes out of scope and is dropped.
After calling takes_ownership
, if my_struct
is used again, an error occurs because the MyStruct
type does not implement the Copy
trait. However, for the variable x
, since it is an integer and implements the Copy
trait by default, it can still be used after calling makes_copy
.
Ownership can also be returned to the calling function through return values, not just by passing it as a parameter to a called function. For example:
#[derive(Drop)]
struct A {}
fn main() {
let a1 = gives_ownership(); // gives_ownership moves its return
// value into a1
let a2 = A {}; // a2 comes into scope
let a3 = takes_and_gives_back(a2); // a2 is moved into
// takes_and_gives_back, which also
// moves its return value into a3
} // Here, a3 goes out of scope and is dropped. a2 was moved, so nothing
// happens. a1 goes out of scope and is dropped.
fn gives_ownership() -> A { // gives_ownership will move its
// return value into the function
// that calls it
let some_a = A {}; // some_a comes into scope
some_a // some_a is returned and
// moves ownership to the calling
// function
}
// This function takes an instance some_a of A and returns it
fn takes_and_gives_back(some_a: A) -> A { // some_a comes into
// scope
some_a // some_a is returned and moves
// ownership to the calling
// function
}
In the gives_ownership
function, we define a variable some_a
and return it, transferring ownership back to the calling function. In the main
function, we define a2
, which is then passed to the takes_and_gives_back
function, transferring ownership again, and finally, it is returned back. This pattern can be redundant, but Cairo provides us with references and snapshots that allow us to use the variable value in the called function without transferring ownership.
Summary
Ownership is an important concept in Cairo. Although it may seem difficult to understand at first, it actually helps us avoid many runtime issues at the compilation level and is a powerful mechanism.
Low latency and free Starknet node awaits!
For a limited time, Reddio is offering unrestricted access to its high-speed StarkNet Node, completely free of charge. This is an unparalleled opportunity to experience the fastest connection with the lowest delay. All you need to do is register an account on Reddio at https://dashboard.reddio.com/ and start exploring the limitless possibilities.
You can discover why Reddio claims the fastest connection by reading more here.