Managing Memory
Memory management is an essential part of programming. It involves allocating and deallocating memory to store data during the execution of a program. In Glu, memory management must be handled explicitly by the programmer, as Glu does not have automatic memory management like garbage collection.
In this chapter, we will explore the basics of memory management in Glu, including how to allocate and deallocate memory, manage memory leaks, and use references and pointers effectively.
Stack and Heap
In Glu, memory is divided into two main regions: the stack and the heap, just like in many other programming languages.
Stack memory is used for storing local variables and function call information. It is managed automatically by the compiler and is fast but limited in size. Stack memory is allocated when a function is called and deallocated when the function returns.
Heap memory is used for storing dynamically allocated memory, such as objects and arrays. It is managed explicitly by the programmer and is slower but can grow as needed.
Stack Allocation
Allocating memory on the stack can be done by declaring variables inside a function. For example:
1
2
3
func allocateInteger() {
var x: Int;
}
In this example, the variable x
is allocated on the stack when the function allocateInteger
is called. The memory for x
is automatically deallocated when the function returns.
Variable Addresses
In Glu, you can get the memory address of a variable using the &
operator. Pointers are used to store memory addresses. Pointers can then be used to access or modify the value stored at that memory address, using the pointer dereference operator .*
.
1
2
3
4
5
6
7
func main() {
var x: Int = 42;
var address: *Int = &x;
std::print(address.*); // Output: 42
address.* = 21;
std::print(x); // Output: 21
}
Note that you can only get the address of a variable allocated on the stack, which is not the case for let
constants.
Heap Allocation
Allocating memory on the heap can be done using the std::alloc
function. The alloc
templated function allocates memory for a single value of the specified type on the heap and returns a pointer to that memory.
1
2
3
4
5
6
func main() {
var x: *unique Int = std::alloc<Int>();
x.* = 42;
std::print(x.*); // Output: 42
std::free(x);
}
The std::alloc
function returns a unique pointer, which is a linear type: it must be transferred exactly once. This way, the compiler ensures that the memory is only freed once and that there are no memory leaks.
If you need to leak the memory, you can use the release
function to release the memory from the unique pointer. This can be necessary when you want to transfer ownership of the memory to another part of the program, which uses a different memory management strategy. The release
function consumes the unique pointer and returns the raw pointer, which can then be used to free the memory manually.
1
2
3
4
5
6
7
8
9
func main() {
let x: *unique Int = std::alloc<Int>();
x.* = 42;
std::print(x.*); // Output: 42
let raw: *Int = std::release(x);
// Do something with raw
// Note: The compiler will not check if raw is freed correctly.
// Only use this if you know what you are doing.
}
Unique pointers are a powerful feature of Glu that allows you to manage memory safely and efficiently. By using unique pointers, you can ensure that memory is deallocated correctly and avoid common memory management issues like double-free errors and memory leaks.
Unique pointers can be transferred between functions using the move semantics, which allows you to pass ownership of the memory from one part of the program to another without copying the memory itself. Unique pointers can also be converted implicitly to raw pointers, which allows you to interact with C libraries or other parts of the program that expect raw pointers. This is necessary when the pointer needs to be borrowed, instead of transferred.
Here is an example of transferring a unique pointer between functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// This returns a unique pointer to an integer, which means that the ownership of the memory is transferred to the caller.
func createCounter() -> *unique Int {
let counter: *unique Int = std::alloc<Int>();
counter.* = 0;
return counter;
}
// This takes any pointer to an integer and increments the value stored at that memory address.
func incrementCounter(counter: *Int) {
counter.* += 1;
}
func main() {
let counter: *unique Int = createCounter();
incrementCounter(counter);
std::print(counter.*); // Output: 1
std::free(counter);
}
In this example, the createCounter
function allocates memory for an integer on the heap and returns a unique pointer to that memory, which means the caller is responsible for it. The incrementCounter
function borrows a raw pointer to an integer and increments the value stored at that memory address. The main
function creates a counter, increments it, and prints the result. As the memory is transferred to the counter
binding within the main
function, it is the responsibility of the main
function to free the memory at the end.
Move semantics means that the binding counter
in the main
function takes ownership of the memory allocated in the createCounter
function. When counter
is passed to the incrementCounter
function, it is borrowed as a raw pointer, and the ownership is not transferred. This allows the incrementCounter
function to modify the value stored at that memory address without taking ownership of the memory. The free
function finally takes ownership of the memory to deallocate it, ensuring that no pointer to the memory is left after the memory is freed.
Note that move semantics also exist within the same function. For example, you can move a unique pointer to another binding within the same function, which means the first binding is no longer valid:
1
2
3
4
5
func main() {
let counter: *unique Int = createCounter();
let moved: *unique Int = counter;
std::free(moved);
}
In this example, the counter
binding is moved to the moved
binding, which means that the counter
binding is no longer valid. The ownership of the memory is finally transferred to the free
function. Note that free(counter);
would not compile in this case, as the ownership of the memory has already been transferred to the moved
binding.
Allocating Arrays
You can also allocate arrays on the heap using the same std::alloc
function. The alloc
function takes an optional argument for the number of elements of the specified type to allocate in a contiguous array of memory, and default-initializes each element. A unique pointer to the first element of the array is returned.
1
2
3
4
5
6
let array: *unique Int = std::alloc<Int>(10);
std::assert(array[0] == 0);
std::assert(array[9] == 0);
array[0] = 42;
std::print(array[0]); // Output: 42
std::free(array);
In this example, the alloc
function allocates an array of 10 integers on the heap and returns a unique pointer to that memory. The elements of the array are default-initialized, the default value for integers being 0. The array can be accessed and modified using the subscript operator []
. Just like with single values, the memory for the array must be freed using the free
function for the code to compile.
Arrays can also be resized using the std::realloc
function. The realloc
function takes a pointer to an existing array, the new size of the array, and returns a new pointer to the resized array. Because both are unique pointers, the compiler can check that the old pointer is not used after the reallocation.
1
2
3
4
5
let array: *unique Int = std::alloc<Int>(10);
array[8] = 42;
let second: *unique Int = std::realloc(array, 20);
// array cannot be referenced anymore, as it was moved to second
std::assert(second[8] == 42);
Shared Pointers
In addition to unique pointers, Glu also supports shared pointers, which allow multiple parts of the program to share ownership of the memory. Shared pointers are reference-counted, which means that the memory is deallocated when the last reference to it is dropped.
Shared pointers can be created using the alloc_shared
function, which allocates memory for a single value of the specified type on the heap and returns a shared pointer to that memory.
1
2
3
let x: *shared Int = std::alloc_shared<Int>();
x.* = 42;
std::print(x.*); // Output: 42
Unlike unique pointers, shared pointers can be copied and cloned, which allows multiple parts of the program to share ownership of the memory. When a shared pointer is copied, the reference count is incremented, and when a shared pointer is dropped, the reference count is decremented. When the reference count reaches zero, the memory is deallocated.
The reference counting is done automatically when copying and dropping shared pointers, so you don’t need to worry about managing the memory manually. This makes shared pointers a convenient and safe way to manage memory in Glu.
Conclusion
Memory management is a critical aspect of programming, and Glu provides powerful tools to help you manage memory efficiently and safely. By using unique pointers and shared pointers, you can allocate and deallocate memory on the heap, transfer ownership of memory between parts of the program, and avoid common memory management issues like double-free errors and memory leaks. By understanding the basics of memory management in Glu, you can write more robust and reliable programs that make efficient use of memory resources.