Joined Odin
Joined Odin
Joined Odin
In this first chapter we'll write a tiny program and run it. You can think of this is an overview chapter for the rest of the
book, as it contains links to things we'll learn later.
package hellope
import "core:fmt"
main :: proc() {
fmt.println("Hellope!")
}
The first line is called the package name. For know, just note that this line must be the same for all files in the same
directory. I will talk more about packages in chapter X.
main :: proc() declares a new proc called main . Proc is short for procedure. Your program can contain many procs,
but the main proc is where the program starts. The program runs from the main proc's opening curly brace { to the
closing curly brace } . I will talk more about procedures in chapter Y, but for now you can think of them as a collection of
code with a name attached.
The only thing the main proc does is print "Hellope!" to the command prompt. It does this using the fmt package. The
line import "core:fmt" fetches the fmt package from the standard library, also known as the core collection. fmt
contains procedures that can print stuff to the screen and also format strings. The stuff inside fmt can then be used by
typing fmt.some_procedure_name() , in this case we type println and put the message we to show between the
parenthesis. I'll go deeper into procedures in chapter A.
The core collection comes with the compiler and contains many packages that are useful in different situations. I will
give a tour of the core collection in chapter Z.
What the . in odin run . refers to, is the current directory. This means that it will take all the .odin files in the
current directory and compile them as a single package, output an executable and run it. This means that if you had two
.odin files in this directory, then they would both be compiled as part of your program.
Compile without running
You you instead execute odin build . , then the program will just be compiled into an executable, without running it.
In both the build and run cases you'll be able to find the resulting executable inside the directory. The name of the
executable will be the name of the directory, as that is the package name.
number: int
This variable is named number and is of type int , short for integer. Note the : between the name and the type, it is the
symbol we use to denote declarations. Odin is a strongly typed language, meaning that the language is strict about types:
You can only put integers into integer variables.
What value does number have? Since we didn't specify any, it will be zero. This is true for all variables of all types. More
specifically, if you do not specify a value then the memory that holds the variable's value will be all zeroes, as we'll see
later this can have a few different meanings for non-numeric types.
number: int
number = 7
Note that doing number = 7 with no pre-existing variable called number will fail to compile. If you want to both declare
and assign a variable on one line, then you can combine the : and the = , like this:
number := 7
In this case you also didn't have to say what type number should have. Instead, the type is inferred from the value on the
right hand side. In this case it is inferred to int .
Here's a small program that creates a variable, prints its value, changes it and then prints it again:
package hellope
import "core:fmt"
main :: proc() {
number := 7
fmt.println(number) // prints 7
number = 12
fmt.println(number) // prints 12
}
Note the usage of := on the line where we declare and initialize number . Also note that we use only = when we later
change its value.
Variable types and casting
You can swap out the int we saw before for a different type. This code declares a decimal number variable:
decimal_number: f32
The f stand for "floating point number", usually called just "float". A floating point number is a method for storing
decimal numbers. The 32 means it uses 32 bits of memory (4 bytes) to store the decimal number. Again, since we are
not specifying a value, decimal_number will be zero.
Previously, when writing number := 7 the compiler inferred number to be of type int . How can we declare and
initialize a variable to 7 in one line, but force the variable to be of type f32 ? There are two ways:
decimal_number: f32 = 7
or
decimal_number := f32(7)
These two variants have the same end result. The first one looks like decimal_number: f32 , but we've added = 7 at
the end. In the second case we have skipped saying what type decimal_number is and instead rely on it being inferred
from the right hand side, which in this case is f32(7) , meaning that we take 7 and cast it, i.e. convert it, to the type f32 .
decimal_number := 7.42
In this case decimal_number will be inferred to type f64 , not f32 . Similarly, number := 7 will be of type int , but if
you specifically want an 8 bit integer, then you must type number: i8 = 7 or number := i8(7) .
You can cast integer variables to float variables and the other way around, here's a small demonstration:
main :: proc() {
number := 7
number = 12
decimal_number := f32(number)
another_decimal_number: f32 = 27.12
another_number := int(another_decimal_number)
}
Note that this would not compile without those explicit casts to f32 and int .
Constants
Consider the following code, which compiles just fine:
number := 7
decimal_number: f32 = 7
However, the following does not compile:
number := 7
decimal_number: f32 = number
7 seems to be of type int since variables are inferred the be of type int when you type
Why is that? At a surface level,
number := 7 . But you can still assign 7 to a variable of type f32 without an error.
The answer is that 7 is not of type int . It is a constant, and constants have a slightly different system for types than
variables do.
Constants are things that only exist at compile-time. Since they only exist at compile-time, they cannot be changed. I
already said that a plain number like 7 is a constant. But you can also create a named constant like this:
CONSTANT_NUMBER :: 12
Note that we use :: to create named constants. You can assign a constant to a variable like this:
CONSTANT_NUMBER :: 12
number := CONSTANT_NUMBER
Just like before, the type of number will be inferred to int . The two lines above are actually identical to just typing
number := 12 . You can think of constants as just being "copy-pasted" into where-ever you type their name. This
means that you can also think of a stand-alone 12 as being a nameless constant.
Now, I said that constants have a slightly different type system compared to variables. One could say that constants have
a slightly weaker type system. The type of CONSTANT_NUMBER :: 12 is Untyped Integer. An Untyped Integer can be
implicitly cast to any integer or floating point type as long as that variable can "accommodate" it. This is why
decimal_number: f32 = 7 is allowed.
What's up with the "accommodate" part above? Here's something that will not compile:
BIG_CONSTANT_NUMBER :: 100000000
small_number: i8 = BIG_CONSTANT_NUMBER
small_number is an 8 bit integer, the biggest value it can contain (i.e. accommodate) is 127 . BIG_CONSTANT_NUMBER is
a constant of type Untyped Integer. So when the compiler tries to shove it into small_number it sees that the value of
this constant is bigger than 127 , so it refuses to do so.
When declaring new variables with a constant in on right hand side, such as number := 7 , then the Untyped Type of the
constant has a default for which variable type to choose. This is why number := 7 infers the type of number as int ,
and also why decimal_number := 27.12 infers the type of decimal_number as f64 . But as we have seen, Untyped
Types also allow for some implicit casts, which is why decimal_number: f32 = 7 is allowed: 7 is an Untyped Integer,
which may be implicitly cast to f32 .
Constants containing a decimal point such as DECIMAL_CONSTANT :: 27.12 are of type Untyped Float. An Untyped
Float can only be assigned to a floating point variable such as f16 , f32 or f64 . This is why this does not compile:
DECIMAL_CONSTANT :: 27.12
my_integer: int = DECIMAL_CONSTANT
When using this constant, no implicit type conversions will happen. You'll need to cast the constant to be able to use it
as a f16 or f64 . Note: I have never used an explicitly typed constant in my own code.
In both cases the type comes after the first : , and then you put a : or a = after the type depending on if you want a
constant or a variable. In other words, you can think of the CONSTANT :: 7 syntax as telling the compiler to infer the
Untyped Type of the constant from the right hand side.
Strings
string is a type that lets you store text. This code creates a string variable and assigns a value to it:
my_string: string
my_string = "Hellope!"
my_string := "Hellope!"
Strings in Odin use UTF-8. This is different from using ASCII, where characters always consume 1 byte of memory. In
UTF-8 a character can be of varying memory size. English characters still just use single byte, but characters from other
languages can use more memory. If you want a quick overview of this, then you can read this entertaining article.
The individual UTF-8 characters have a type of their own called rune , there are examples on working with strings and
runes in chapter 12 FIX LINK.
A_STRING_CONSTANT :: "Hellope!"
fmt.println(A_STRING_CONSTANT)
String constants have the type Untyped String. These constants can be implicitly cast to both the type string and also
the type cstring . cstring is for interfacing with libraries written in the C programming language.
In the package core:strings you'll find lots of nice procs for working with strings. I'll talk about some of them later.
Signed integers:
Unsigned integers:
Boolean types:
bool b8 b16 b32 b64 where the number is the number of bits to use.
Possible values:false or true .
Zero value: false .
bool is equivalent to b8 , i.e. it takes 1 byte of memory.
The specific b32 etc variants are there mostly for interfacing with other programming languages. Just use bool in
your Odin code.
Strings:
string
Possible values: Text encoded as UTF-8
Zero value: Empty string, i.e. ""
Knows how long the string is, i.e. does not use null-termination.
There's also a cstring type for interfacing with the C programming language. cstring is null-terminated. There
are procs in core:strings to convert back and forth between string and cstring .
Runes:
rune
A single UTF-8 character
Untyped Booleans:
Untyped Integers:
The type of any numeric constant without a period
Inferred type of some_variable := 7 is int (equivalent to i64 on 64 bit systems).
Can be implicitly cast to any integer or floating point type given that the type can accommodate the value, for
example some_variable: i8 = 10000 will not compile.
Untyped Floats
Untyped Strings
The type of any string constant, i.e. something between two quotation marks "a_string" or between two
backticks `can be multi-line` .
Inferred type of some_variable := "Hellope" is string
Can be implicitly cast to any string, i.e. the types string or cstring
Finally, a note about implicit casts from Untyped Float / Untyped Integer to a floating point variable: some_variable:
f16 = 1000000000 (note the f16 type) will compile, but the value is bigger than what an f16 considers "infinity".
Therefore some_variable will have the value "positive infinity".
Arithmetic
As in many languages you can add, multiply and divide numeric variables and constants:
number := 7
number = number + 1 // You can also write number += 1
second_number := 2
third_number := number * second_number
fmt.println(third_number) // Prints 16
Due to how the constant systems works, Untyped Integers are implicitly cast when for example adding them to float
variables:
and also any Untyped Float can be added to any float variable:
some_ints: [20]int
Like always, all the 20 ints in this fixed array will have the default value zero.
some_ints: [20]int
some_ints[3] = 500
some_ints[4] = 200
some_ints[5] = 333
// prints 200
fmt.println(some_ints[4])
You can iterate over a fixed array, meaning that you can easily loop over all the items:
for v in some_ints {
fmt.println(v)
}
If you need the index while looping, then add in another loop variable after v:
for v, i in some_ints {
// this will print `value at 0 is Name` etc
fmt.printfln("value at %v is %v", i, v)
}
Note that v is immutable in these loops. If you want to modify the value then add in & in front of the loop variable:
position: [3]f32
This creates a variable position that is an array of three f32 numbers. Change the value of it like so:
position: [3]f32
position = {0, 1, 0}
or all at once:
You can fetch the different values from position like so: position[0] . However, when arrays have 4 or fewer items,
then you can index them using x , y , z and w as well. So instead of position[0] you can write position.x .
You can also create a smaller array from a bigger one using these xyzw letters:
zx_pos := position.zx
will make zx_pos into a variable of type [2]f32 that contains the z and x parts of the position. You can also do stuff like
position.xxyy or position.zyx . Finally, because in some applications we use 4 dimensional numbers to represent
colors, you can also use the letters .rgba . This kind of operation is referred to as swizzling.
If you don't wanna write [3]f32 all the time then you can make an alias called Vector3 :
Vector3 :: [3]f32
This essentially turns Vector3 into a new type that is identical to [3]f32 . We choose the name Vector3 . A 3D vector is
a direction plus a length in 3D space, which is representable using 3 floating point numbers.
You can add arrays that have matching item type and size, like so:
Vector3 :: [3]f32
position: Vector3
velocity := Vector3 { 0, 0, 10 }
position += velocity
This is known as 'array programming'. You can add arrays, subtract them and multiply them with scalars. Odin does not
have operator overloading, but one of the most common use cases for operator overloading is doing vector maths. So I
haven't missed operator overloading at all because of how great array programming works.
Things like this is a good example of why I enjoy Odin so much for video games programming.
Copying fixed arrays 1.2
Fixed arrays always copy the whole array when you assign it to a new variable:
So in this case position and position2 are two separate copies. This clarification is simply meant to show that there
is no shared memory between these two fixed arrays.
Then, if you wanna pass in a hard-coded position, you can do it like so:
There's no need to write the type name since the compiler can figure it out using the type of the parameter.
The above is actually just the standard way of assigning a value to a fixed array:
position: [3]f32
position = {100, 100, 200}
but you can see it as assigning it to the parameter instead of a local variable.
Making new types 1
So far we've seen basic types such as int and f32 . Let's look at how you can group such
basic types into bigger, more complex types called "structs". The benefit of doing this is
that you can pass them around as a single unit, set the structs value as a single unit and give
your program better logical structure. We'll also look at some additional ways to create new
types, such as enums and unions. As we shall see, unions can be used in many different
ways to both save memory but also to create variations within structs.
Structs 1.1
Structs consist of a number of fields, where each field has a type. You can see it as a way to
group variables. Let's say that we want to store the data for a rectangle. That needs a
position in two dimensions (x and y), a width and a height. We can define a new struct that
contains those four things:
Rectangle :: struct {
Coming from C / C++
x: f32,
y: f32, In C/C++ I used to force myself
width: f32, to put a comma even at the last
height: f32, field of a struct. That way,
} when adding a new field, you
didn't forget the comma on the
Each field looks like a variable declaration, but with a comma at the end of each field. previous line, saving you that
compile error. With this in
mind, in Odin commas are
Here we see the double colon ( :: ) again. So far we have seen three different things use the
always enforced, even for the
:: . Those are procedures, constants and now struct definitions. All these things are known last line.
at compile-time, meaning that when you use the identifier to the left of the :: , the the
compiler is able to reason about whatever is on the right side of the :: . So you can think of
:: as a way to do compile-time declarations.
The above just defines a new type Rectangle , i.e. in order to actually use Rectangle to
store any information, we must create a variable of that type:
rect: Rectangle
This is just like earlier when we've done number: int , but the type Rectangle is a more
complicated type than just a single integer.
Coming from C / C++
The four fields x , y , width and height will be all default initialized to zero. Whenever you These initializers where you
create a variable of an type, be it just an int or a big struct, then the whole block of mention fields by names are
memory that that thing uses will be filled with zeroes. known in both Odin and C as
designated initializers. They
are one of my favorite features
If you want to initialize the rect when you create it, then you can do like this:
in C due to it being possible to
mention only some fields and
rect := Rectangle { the non-mentioned will be
width = 20, zeroed. Unfortunately, in C++
height = 10, they have only recently been
} added, so you can't really use
them yet. Not being able to use
This rect will have its width and height fields set to 20 and 10 respectively. Any field not them was always an annoying
mentioned will be zero-initialized. In this case the fields x and y will both be zero. trade-off when I needed to use
C++ instead of C.
But doing rect := Rectangle { whenever possible is usually considered more idiomatic
For example when initializing a
Odin. Doing so will make your code look more consistent since there will be cases when you
union or passing a struct to
must do Rectangle { /* initializers */ } without there being a variable. some procedure.
You can replace the value of a whole struct by assigning to it, effectively re-initializing it:
rect := Rectangle {
width = 20,
height = 10,
}
// a bit later
rect = {
width = 5,
height = 7,
}
Also note that we did not have to supply the type-name Rectangle on the rect = { line.
The compiler sees that we are assigning to a variable of type Rectangle so we do not need
to type it.
You can also initialize structs by just writing the values directly, without providing the
names of the fields:
rect := Rectangle {
20, 20,
200, 200,
}
This sets x , y , width and height of our Rectangle to the values in the order they are
listed. When initializing in this way, without using specific names, then you must list all the
fields, in this case you must provide four numerical values since there are four fields in
Rectangle .
Person_Stats :: struct {
health_score: int,
age: int,
}
Person :: struct {
stats: Person_Stats,
name: string,
}
p: Person
then p will be of type Person and all the fields inside it will be zero. The Person_Stats
inside it will also be all zeroes. The memory for the stats field lives directly within the
Person_Stats is 2 * 8
Person struct, there is no separation between the memory of the two. Having stats:
bytes = 16 bytes on a 64 bit
Person_Stats in there makes the memory Person takes bigger by however big the computer, because int takes
Person_Stats struct is. 64 bits = 8 bytes in a 64
bit computer.
You can do nested initialization of the Person_Stats inside the Person :
p := Person {
stats = {
health_score = 7,
},
name = "Bob",
}
You can fetch and set the fields inside the struct using . (a period):
p := Person {
stats = {
health_score = 7,
},
name = "Bob",
}
p.name = "Bobinski"
p.stats.age = 36
bobinskis_health_score := p.stats.health_score
Note the last line: We are creating a new variable by fetching the health_score field. The
type of it will be inferred to int since that's the type of that field.
You can use the using keyword to make the fields of a struct that lives within another
struct directly accessible:
Person_Stats :: struct {
health_score: int,
age: int,
}
Person :: struct {
using stats: Person_Stats,
name: string,
}
The only difference between this and the earlier definition of Person is that there is now a
using in front of stats: Person_Stats .
You can now use the fields of stats directly on a Person object:
p := Person {
health_score = 20
}
p.age = 70
This using is not to be confused with " using on variables and parameters", which I
discourage you to use in the chapter on things in Odin to avoid.
Entity :: struct {
name: string,
position: Vector2,
}
Then you can create other structs that you want to "be entities" by putting using entity:
Entity at the top of those structs. Imagine that we want to have a player character, that is
also an entity, but that has some additional info that is unique to the player:
Player :: struct {
using entity: Entity,
can_jump: bool,
}
Now we can create a player and use the name and position on it directly:
p := Player {
position = some_position,
can_jump = true,
}
Furthermore, you can also use something of type Player as if it was of type Entity:
p := Player {
position = some_position,
can_jump = true,
}
If you come from something like C++ or C#, then you might be used to the concept of a
class where you can define procedures within structs, which are usually referred to as
methods. There are no classes or methods in Odin, only structs and separate procedures. A
procedure cannot be defined within a struct. Use structs to store data and procedures to
process data.
Note that you can do something like this in order to store a proc within a struct:
My_Interface :: struct {
required_name_length: int,
is_valid_name: proc(My_Interface, string) -> bool,
}
my_interface := My_Interface {
required_name_length = 5,
is_valid_name = my_proc,
}
But in this case each object of type My_Interface can have a different value of the field
By "compatible" I mean that
is_valid_name . It's just a field that is a procedure, and you can assign any compatible
the procedure has the same
procedure to that field. But it's not part of the definition of the type what that procedure
signature: Same parameters,
actually does. return values and calling
convention.
I do not recommend doing the above in order to 'implement methods'. The above is useful
in some cases where the actual implementation of a procedure is unknown or somehow
abstract. However, this is very rarely the case.
In general, just use structs and separate procedures. Embrace this, it will make your code
nice and simple.
You can define new enum types. Each enum type declares a series of named values, which
are actually just named numbers.
Computer_Type :: enum {
Laptop, // has value 0
Desktop, // has value 1
Mainframe, // has value 2
}
ct: Computer_Type
To this variable ct you can assign any of the three values we listed in Computer_Type .
Since we didn't initialize ct to a specific value, it is zero-initialized. As we see in the
definition of Computer_Type , the first value Laptop is associated with the number 0 . This
means that ct got the value Laptop when it was zero-initialized.
ct = .Mainframe
You can also write the full Computer_Type.Mainframe if you want to, but since the
compiler knows the type of ct , you just have to write the name of the enum value you
want, with a period in front.
Switch 1.2.1
You can use switch to branch on the type of the enum and do different things depending
on the value:
switch ct {
case .Laptop:
You can also use if-statements to do the same: then you write fallthrough
if ct == .Laptop {
} else if ct == .Desktop {
} else if ct == .Mainframe {
However, the switch version is less chatty and usually faster since the compiler can
generate simpler code for it.
A switch must list all the enum values. If you want to skip some enum values, then put
#partial in front of switch :
#partial switch ct {
case .Laptop:
case .Desktop:
}
I avoid #partial unless I have a good reason to use it. Why? If I add new a new value to the
enum, then I probably want to know all the places it is used. By avoiding the #partial then
I'll get a compile error for any unhandled case , which is a good way to find places where I
need to fill in some code for that enum value. If you don't need to do anything for a specific
enum value, then you can leave something like case .The_New_Value: that does nothing.
Computer_Type :: enum {
Laptop = 1,
Desktop,
Mainframe,
}
then the first enum value .Laptop has the value 1 and then it counts from there. The
following two will thus have value 2 and 3 .
You can set explicit numbers for all of them if you wish.
Computer_Type :: enum {
Laptop = 1,
Desktop = 2,
Mainframe = 4,
}
Also, any time you specify a number, then any enum values without a number that follow it
will count in from there:
Computer_Type :: enum {
Laptop = 0, // value 0
Desktop, // value 1
Mainframe = 5, // value 5
Broken, // value 6
Gamer_Rig, // value 7
}
Animal_Type :: enum u8 {
Cat,
Rabbit,
}
In most cases, I recommend just going with the default of int . An example of a situation
where I would use a specific backing type is when I create bindings to libraries written in
other languages, as the size of the enum must match what the library expects.
Unions 1.3
While enums let us say what something is using the named enum values, it doesn't let use
store any additional data related to the current enum value. Unions in Odin let you store
both what something is and also the data associated with it. Here's an example:
My_Union :: union {
f32,
int,
Person_Data,
}
Person_Data :: struct {
health: int, Coming from C / C++
age: int, This kind of union is called a
} tagged union and it's a more
powerful concept that the
val: My_Union = int(12) union in C. We will discuss this
more in a bit.
Note that the three fields inside union are type names. We are saying that this union can
hold a value of either a f32 , int or struct type Person_Data . The union stores both which
type it is currently storing and the actual data for that type. In the example above we create
a new variable val of the union type My_Union and assign to it int(12) . The union then
knows that it contains an int and it also knows that the value is 12 .
You can assign a new value to val , for example a Person_Data object: Note that you cannot do
These different possible values of the unions are usually referred to as the variants of the
union.
The My_Union type will only take as much memory as the biggest variant. Since it can only
contain one of the variants at a time, it can use the same block of memory for all of them.
My_Union memory layout: f32 , int and Person_Data takes 4, 8 and 16 bytes of memory
respectively, but they all start at the same address. This is how the union only takes as much space as
the biggest variant. Note that int is actually 4 bytes on 32 bit systems.
Therefore the union just contains a block of memory that is as big as the biggest type,
which in this case is Person_Data , which is 16 bytes large. You can think of it as three
different variables that all share the same memory, but you're only allowed to use one of
them at a time.
Just like with an enum you can use switch to branch on what value it currently holds:
switch v in val {
case int:
// you can use v, it is of type int
case f32:
// you can use v, it is of type f32
case Person_Data:
// you can use v, it is of type Person_Data
// I.e. you can fetch fields within v as well:
fmt.println(v.age)
}
However, if you compare this to the switch statement we used with an enum, then note
these two differences:
Only the case that matches the type of the union's current value will run. And within that
case you'll have access to the value by typing v . So if you end up inside case
Person_Data: then v is of type Person_Data and you can use the stuff inside your Without the & , v is an r-value,
Person_Data struct, by typing for example v.age . while it becomes an l-value
when you add the & in front. l-
values are addressable, which
Note that you can choose any name instead of v.
r-values aren't. The l and r
stand for left and right. Why?
If you wish to change v within the case, then you need to add an & in front of v : On a line like
number = 7
switch &v in val {
case int: then number must be an l-
v = 7 value bacause it is to the left of
the equals sign. 7 is an r-value,
case f32: in this case it is a non-
v = 42 modifiable constant. An r-
value such as 7 can never be on
case Person_Data: the left side, because it makes
v.age = 7 no sense:
} 7 = 2.
However, l-values can appear
Sometimes you're only interested in checking if the union is holding a value of a specific
on the right as well:
type. In that case you can do something like this:
number = some_variable .
f32_val, f32_val_ok := val.(f32) Note that the & for making the
switch variable an l-value has
if f32_val_ok { nothing to do with the & that
// here you can use f32_val takes the address of a value, i.e.
} creating a pointer. You cannot
explicitly choose if something
f32_val_ok is a bool . It will be true if val currently held a value of type f32 . However, is an l or r-value in most cases.
It's only in this specific case
the code above is a bit awkward because it litters the surrounding code with the variables
and when working with some
f32_val and f32_val_ok . To avoid that, I recommend this special type of if-statement:
loops that & does this l-value-
fication.
if f32_val, ok := val.(f32); ok {
// use the value of type f32 that the union held
}
This if-statement both creates the two variables f32_val and ok and the part after the ;
checks the actual condition. f32_val and ok are only available within the if-statement,
not outside of it.
You can also just do f32_val := val.(f32) , but then you are expecting val to hold a
value of type f32 . If it doesn't then your program will assert, i.e. crash with an error.
Finally, if you wish to modify the value, then you can add in a & just before val.(f32) :
if f32_val, ok := &val.(f32); ok {
f32_val^ = 7
} In the big comment above I
said that switch &v in val
In this case adding the & turns f32_val into a pointer. We haven't talked about pointers { just made v into an "l-
yet, but it's essentially an address to a value of type f32 . The f32_val^ = 7 above value" so that it is possible to
changes the value which the pointer f32_val points to. I will discuss pointer more later modify. I emphesized that in
FIX LINK. that case v is not a pointer. But
that's just specific to switch
and for loops. In all other
The zero value of a union 1.3.1 cases & takes the address of
something, creating a pointer.
Say that you have a union of two structs, like so:
Shape :: union {
Shape_Circle,
Shape_Square,
}
Shape_Circle :: struct {
radius: f32,
}
Shape_Square :: struct {
width: f32,
}
At the end of the code above we declare a variable shape of the type Shape . Since we didn't
provide a value it is default-initialized to zero. In this case the zero value is interpreted as
nil . I.e. your union does not have a value of any of the two types.
Shape :: enum {
Circle,
Square
}
In the enum case shape will have the value 0 , which is equal to Shape.Circle .
It is possible to make unions behave in the same way as enums, meaning that it is possible
to make unions use the first variant as the zero value. If we add #no_nil to the declaration
our union, then it cannot be nil , the zero value is then instead the first variant of the
union:
Shape_Circle :: struct {
radius: f32,
}
Shape_Square :: struct {
width: f32,
}
The way a union keeps track of which variant it currently holds is by something known as a
tag.
The tag is a number kept as part of the internal workings of the union. It's just a number
that says which union variant the union currently holds.
Shape :: union {
Shape_Circle,
Shape_Square,
}
0 -> nil
1 -> Shape_Circle
2 -> Shape_Square
When we write
shape: Shape
then the tag is always set to zero, which in this case means that the union is interpreted as
having the value nil .
0 -> Shape_Circle
1 -> Shape_Square
So now when we write shape: Shape and get a zero-initialized union, the tag says it is of
variant Shape_Circle instead. Also, note that the data block that holds the actual memory
for the variants is also zero initialized, so the fields inside Shape_Circle are also all
zeroed.
Maybe 1.3.3
There's a built in type in Odin called Maybe . This type can either have no value, or some
value. It's implemented using a union that has a single variant:
This uses parametric
Maybe :: union($T: typeid) { polymorphism, which we
T haven't talked about yet. But
} you can see it as the int in
Maybe(int) being copy-
So when you type: pasted into where it says T.
time: Maybe(int)
Then time can either be nil or have a value of type int . You can check if time has some
value like so:
if time_val, ok := time.?; ok {
// use time_val
}
This time.? syntax is the same as writing time.(int) , but since the Maybe only has a
single variant then the compiler fills the type in for you.
t := time.?
In that case the program will assert (crash with error) if time is nil.
In C union also exists, but it has no tag, so you have to use an extra variable to keep track of
which variant it currently holds. A common approach in C is to use both an enum and a
union .
In Odin you can create these C-style unions, they are known as raw unions. You create them
by putting #raw_union on a struct:
a_raw_union: My_Raw_Union
struct , not union ! There's no safety here that tells us which variant
Note: We used
a_raw_union currently holds. You can both variants by typing a_raw_union.number or
a_raw_union.struct_val .
What #raw_union does is that it makes all the fields inside the struct start at the same
point in memory, i.e. they overlap. This is the same as the data of the tagged union , but
without the tag, so you have to make to keep some extra variable to keep track of the
current variant.
Struct variants using unions 1.3.5 Sorry non-gamedevs for all the
video game examples. I'm a
Again, say that you're making a video game and you have an Entity struct that represents game developer and these
an object in your game world: examples come naturally to
me!
Entity :: struct {
position: Vector2,
texture: Texture,
can_jump: bool,
time_in_space: f32,
}
Here the first two items sound nice and general: A 2D position that tells us where the entity
is, and a texture (an image) that is used for drawing it on the screen.
However, let's say can_jump is only used when Entity represents the player character,
and time_in_space is only used when Entity represents a rocket flying through space.
We can save space in the Entity struct by moving those things into a union:
Entity :: struct {
position: Vector2,
texture: Texture,
variant: Entity_Variant,
}
Entity_Player :: struct {
can_jump: bool,
}
Entity_Rocket :: struct {
time_in_space: f32, There are people who just keep
} everything inside Entity ! This
can create interesting
Entity_Variant :: union { emergent gameplay since any
Entity_Player, entity can modify any field of
Entity_Rocket, any other entity. Such a non-
} subtyped struct is sometimes
called a "Mega Struct". If
so now when you create an entity, you can set the variant of it to an appropriate type: you're making a small game
with few entities, then a mega
struct it probably feasible.
player_entity := Entity {
position = starting_position,
texture = player_graphics,
variant = Entity_Player {
can_jump = true,
}
}
Earlier I showed how to make a type Entity the parent of some struct by means of the
using keyword. If you compare the two approaches you see that they solve a similar
problem but in different ways. Use the method that works well for you.
We'll return to some additional and more advanced types later in the book, but this should
do for now!
Procedures and scopes 1
We've seen the basics of how to create new procedures. Let's dig in a bit deeper. Here we will look at default values of
proc parameters, multiple return values and named return values. Finally we'll look at what the scope of the proc is, how
scopes within scopes work and how the defer keyword can be used and how it relates to the scope.
Parameters 1.1
In a proc like
we see that the parameters take the shape of number: int . This looks similar to a variable declaration. If you have
multiple parameters of the same type you could also write:
Note how we added a comma between the two parameters of the type and only wrote the type name once.
Note the absence of the scale parameter, it will have the default value { 1, 1 }
All parameters are always immutable. You can never change the value of a parameter. This would not compile:
This re-declaration of variables with the same name (also known as shadowing) is allowed for proc parameters in order
to give you a mutable copy without having to come up with a new name.
Note the r := r . In order to modify the rectangle a mutable copy is first created.
if r.width < 0 {
r.x += r.width
r.width = -r.width
}
if r.height < 0 {
r.y += r.height
r.height = -r.height
}
return r
}
Rect :: struct {
x: f32,
y: f32,
width: f32,
height: f32,
}
It takes a slice of bytes (we'll talk about slices later, you can think of it as an array) and clones it. This cloning needs a
way to allocate memory. You can just run copy := bytes.clone(some_bytes) , and as you see allocator and loc will
then be pre-filled.
We'll talk about the context , temp_allocator and memory allocation in the chapter on memory allocation TODO FIX
LINK.
main :: proc() {
number := add_and_double(2, 4)
fmt.println(number)
}
This program will print 12 . Note how we put -> int at the end of the add_and_double . This says that the proc will
return one value of type int . return is then used within the proc actually return a value back to the caller. In main we
then call add_and_double with two numbers. We store the return value of add_and_double into number , which we
then print.
Since the return value of add_and_double is int , the type of number will be inferred to int .
main :: proc() {
number, ok := divide_and_double(2, 4)
if ok {
fmt.println(number)
}
}
Here divide_and_double divides the first number by the second and then multiples the result by 2 . But it checks if the
second value is 0 , in which case it returns 0 in the first return value and false in the second return value. This is
because dividing by zero is not mathematically OK. However, if the second value is non-zero then it performs the
calculation and returns the result in the first return value and true in the second return value.
This way the second return value can be used to check if everything went OK.
Here the first part before the ; runs divide_and_double and sets the up the two variables number and ok , and then
the stuff after the ; is the actual condition of the if-statement. This is nicer because
number and ok will only be
accessible within this if-statement, the surrounding code will not be see them.
res = (n/d)*2
ok = true
return
}
The named return values res and ok act just like normal variables. They are zero-initialized by default. This means that
res is 0 and ok is false .
So in cased is 0 , which means we are about to divide by 0 , then we just write return . This will return res = 0 and ok
= false , which is what we want.
These "naked return" (i.e. a return without specifying what is returned) means that we are returning the value in res
and ok . We can still do explicit returns when we have named return values, i.e. return (n/d)*2, true would also
work.
Later TODO FIX LINK, when we talk about error handling we will make use of named return values in combination with
or_return .
my_number := 7
increase_number(&my_number, 3)
// my_number is now 10
Here we are passing a pointer to my_number into the proc increase_number so that the proc can alter it. The ^ in front
of ^int means that we want a "pointer to an integer". We get a pointer to something by taking the address of it, using
the & operator: increase_number(&my_number, 3)
Within the proc you can add amount to the variable that the pointer points to by putting a ^ on the right side of n , as we
see in the n^ += amount line. This means that something like n^ = 123 would take the address that n contains and
modify the integer at that address, in this case setting it to 123 .
You can also pass structs by pointer and modify its fields:
Player :: struct {
position: Vector3,
health: int,
}
player := Player {
health = 100,
}
damage_player(&player, 10)
// player.health is now 90
Now, say that we want to pass this player struct to a proc. But the proc will not modify the player, so we do not need to
pass it by pointer. How would that look? Like this, i.e. just like we've passed values into procs before:
Player :: struct {
position: Vector3,
health: int,
}
Box :: struct {
position: Vector3,
size: Vector3,
}
player := Player {
health = 100,
}
collider := player_collider(player)
Here we take the player and return a new struct that tells us how 'big' the player is by combining the position of it and a
size.
But now those who come from C/C++ might say: Hang on! Doesn't this copy the whole player struct each time you call
player_collider(player) , since you do not pass it by pointer?
The answer is no, as long as the player struct is larger than pointer size (8 bytes on 64 bit machines), it will
automatically be passed as an immutable reference. This means that from within player_collider the parameter
player is the exact same memory as the player variable we passed in. But since it is immutable you cannot change it,
trying to alter any of the fields of player is a compilation error.
The scope of the proc 1.4
The scope of a procedure runs from the opening curly brace { to the closing one } :
my_proc :: proc() {
number := 7
}
This means that number in the code above is only valid within that scope. The scope has its own stack memory upon
which local variables like number live.
This code will compile, but the return value of it is "pointer to integer". The value we are returning is the address of
number , but that variable lives within the scope of the proc . So once the procedure is over, then the pointer we are
returning is pointing at garbage memory that may soon be used by other parts of the program.
my_proc :: proc() {
number := 7
if number > 5 {
message := "It's more than 5"
fmt.println(message)
}
my_proc :: proc() {
number := 7
{
message := "Hellope"
fmt.println(message)
}
Anonymous scopes can be useful in long procs where you would otherwise get variable name collisions.
result: rl.Image
/*
Here goes some code that creates the 'result'
image based on to_process.
*/
return result
} // <-- rl.UnloadImage(to_process) happens here.
Here rl.LoadImage load the image to process from disk. Immediately after that line we see defer
rl.UnloadImage(to_process) . The defer will make this line run at the end of the current scope, in this case at the end
of the process_image procedure. This way we can group the code for loading and unloading the image. This can be
useful in cases where there is a lot of code between the line where you load the image and the line where you unload it.
Another good example, from the overview, is to defer the closing of a file handle.
f, err := os.open("my_file.txt")
if err != os.ERROR_NONE {
// handle error
}
defer os.close(f)
// rest of code
As I've pointed out, the scope is not necessarily the scope of the current proc. If you put the defer within an anonymous
scope or within a block of an if-statement, then the defer will execute at the end of that scope, not at the end of the proc.
Containers
We've already looked at fixed arrays. In this chapter we'll meet all the other types of containers available in Odin:
Dynamic arrays, slices, enumerated arrays and maps.
We will also take some first looks at how to use dynamic memory: We'll talk about when memory will be allocated and
how to deallocate it. We won't go much into using custom allocators and the temp allocator in this chapter, we'll save
that for the chapter on manual memory management TODO FIX LINK.
Dynamic arrays
When we looked at fixed arrays I said that they are, as the name suggests, of fixed size and cannot grow. If you want an
array that can grow as your program runs, then you can use a dynamic array.
You create an empty dynamic array that holds integers like this:
some_ints: [dynamic]int
append(&dyn_arr, 5)
Note the & , append needs a pointer to make changes to the array. You can swap out int for any type you'd like to use.
Initially the dynamic array is empty. So when you try to append an element, then it will need to grow. Growing means
that it needs some way to allocate memory. While a fixed array's size is known at compile-time, a dynamic array's size
is not. Therefore it will dynamically allocate memory.
append will use an allocator to somehow acquire this memory. We'll talk more about allocators in the chapter on
manual memory management, but for now just note that there is a default allocator available everywhere:
context.allocator , which is a heap allocator.
data: rawptr,
len: int,
cap: int,
allocator: Allocator,
So when you write dyn_arr: [dynamic]int then the data , len , cap and allocator will all be default initialized to
zero. Later, when append is run, it sees that data is 0 (or nil , which is the zero value for pointers). It will therefore try
to allocate some initial memory for the array. It will check if the dynamic array's allocator field is set and use that
allocator to allocate the memory. But in our case the allocator field is zeroed, so it will instead fall back to using
context.allocator and ask that allocator for some memory. It will also make sure to store context.allocator in the
empty allocator field.
After the allocation is done, then the cap field will hold the current capacity of the dynamic allocator, and len will tell
you how much of that capacity you've actually used up. So for each time you a append an item, len is increased by 1 .
When len and cap becomes equal, then append will try to allocate more memory. This time the allocator field has a
value so it can go straight for that one.
Note: With the exception of the allocator field above, you cannot actually fetch the other fields, such as cap and len .
Those things are just how dynamic arrays look internally. Instead, if you need the length or capacity, use
len(array_name) and cap(array_name) . You can use len(array_name) with all other kinds of arrays as well.
Removing items
You can remove items from a dynamic array in two ways:
unordered_remove(&dyn_arr, index)
unordered_remove removes an element at position index , but it does it in an unordered way. This means that the order
of the dynamic array might not be the same before as after the remove.
This means that it takes the last element and copies it to the place where you are trying to remove something and then
makes the array one element shorter.
If you need to preserve the order of your dynamic array, then instead use:
ordered_remove(&dyn_arr, index)
This procedure will instead move everything after the removed item back one index and thereafter decrease the length,
which is more expensive.
Always use unordered_remove unless the order matters! It is less computationally expensive since it only has to copy a
single element.
delete(dyn_arr)
Be sure that you do not need anything in the dynamic array before you run delete . Choosing when to actually run
delete is what some people find a bit tricky about manual memory management. We will talk more about manual
memory management strategies in a later chapter TODO FIX LINK.
Other than freeing the memory, delete will not modify your dynamic array. If you wish to reuse the same dynamic
array after the delete, then it might be a good idea to zero it out after the delete:
dyn_arr = {}
This will once more make the fields inside the dynamic array all zeroes. However, in many cases a delete + zeroing
actually means that you just need to clear the array. Clearing the array sets the len inside the dynamic array to 0:
clear(&dyn_arr)
clear does not free any memory. cap will still be whatever value it was before and the allocated memory will still be
I.e.
around. It will just set len to 0 so that the dynamic array starts of from the beginning.
Iterating dynamic arrays
Just like with fixed arrays you can iterate a dynamic array:
You can skip the index loop variable if you do not need the index of the current element.
If you need to modify the value during the loop, then add an & in front of the value:
Which will create a dynamic array with the length0 , but with capacity for 20 items. This means that the make proc will
allocate memory immediately, not at the first append . This also means that it won't have to grow the dynamic array
until you've added 20 things to it.
If you do this:
cap and the len will be 20 . This means that the dynamic array will start with 20 zeroed out items. I.e.
Then both the
dyn_arr := make([dynamic]int, 20) is equivalent to dyn_arr := make([dynamic]int, 20, 20)
Entity :: struct {
position: [3]f32,
}
entities: [dynamic]Entity
append(&entities, Entity { position = { 1, 2, 3 } })
entity_ptr := &entities[0]
for i in 0..<64 {
append(&entities, Entity {})
}
entity_ptr.position = { 4, 5, 6}
The first call to append will grow the array. After that a pointer to the newly added item is stored in a new variable.
Thereafter it appends 64 more elements. Appending 64 more elements is gonna make the dynamic array grow again.
When it grows it allocates new memory for the dynamic array and copies all the old data there and then frees the old
memory. Therefore, your entity_ptr is now pointing at a memory address that lives in the old, freed memory. Using it
may cause your program to crash.
Try to not store pointers to elements in a dynamic array. A better idea is to store a handle. A handle can be something
simple like the index of the element. Usually one also has some extra data to know that the item at the index is still the
same (it hasn't been replaced). You can read more about these ideas on Andre Weissflog's blog.
my_numbers: [50]int
Now, say that you only want to look at the first 20 numbers of my_numbers , then you can fetch those by slicing
my_numbers :
my_numbers: [50]int
first_20 := my_numbers[0:20]
A slice is a 'window' that looks at a part of an array. Slices are essentially pointers plus a length, where the pointer says
where the slice starts. Creating a slice does not allocate any memory, it uses the same memory as the thing you sliced,
but looks into just a part of it. We create a slice using the [:] operator, where you put in the indices you want it to span
like this: [start_index:end_index]
my_numbers: [50]int
last_20 := my_numbers[30:]
In this case we create a slice that runs from index 30 to the last index. Omitting the first index would make it run from
index 0, for example: first_20 := my_numbers[:20] . If you skip both indices then you get a slice that looks at the
whole thing:
my_numbers: [50]int
full_slice := my_numbers[:]
This is useful when you have a proc that expects a slice, but you have a fixed size array or dynamic array.
Slice example: Considering 10 elements at a time
As an example, say that you only want to display 10 numbers at a time from a bigger list of numbers. You could do this
using slices:
my_numbers: [128]int
Slices can be formed from different kinds from arrays, other slices and also dynamic arrays.
my_numbers: [128]int
first_20 := my_numbers[:20]
last_10_of_first_20 := first_20[10:]
That is, I first created one slice called first_20 and then made a second slice by slicing first_20 .
a_dynamic_array: [dynamic]int
for i in 0..<100 {
append(&a_dynamic_array, i)
}
first_50 := a_dynamic_array[:50]
my_numbers: [128]int
first_20 := my_numbers[:20]
first_20_clone := slice.clone(first_20)
It will allocate memory using context.allocator and copy the data of first_20 into that memory. This will make
first_20_clone have its own, dynamically allocated memory. This means that you'll have to run
delete(first_20_clone) in order to deallocate that memory.
It is also possible to create a slice from scratch and make it have it's own memory:
You can see this as a way to dynamically allocate an array with a fixed size. I.e. doing my_numbers: [128]int does not
allocate any dynamic memory, but the size has to be known as compile time. my_numbers := make([]int, 128) gives
you something similar, but the memory is allocated dynamically and the size can be any integer value or variable.
Enumerated arrays
You can create arrays that map nicely to an enum and that always has the same number of fields as an enum:
Nice_People :: enum {
Bob,
Klucke,
Tim,
}
nice_rating := [Nice_People]int {
.Bob = 5,
.Klucke = 7,
.Tim = 3,
}
bobs_niceness := nice_rating[.Bob]
You can also skip initializing it, then it will still have the same number of items as the enum, but all will be zeroed:
nice_rating: [Nice_People]int
Finally, you can do a partial initialization, all non-mentioned variants will be zero:
Maps
Maps are in some languages referred to as dictionaries. This kind of container maps keys to values. Declare a new map
like this:
age_by_name: map[string]int
This map has keys of type string and values of type int .
karls_age := age_by_name["Karl"]
If there was no value with that key, then karls_age will be zero. If this isn't good enough, then the you can also get a
second value of type bool that says if the item existed or not:
if karls_age, ok := age_by_name["Karl"]; ok {
// There was a value for this key! In here you can safely use `karls_age`.
}
There are also two special operators called in and not in that you can use to check if a key exists in the map:
Iterating maps
You can iterate maps using
You can make the value modifiable by adding in a & in front of value :
Pre-allocating maps
Just like with dynamic arrays, if you write some_map: map[string]int , then some_map will be all zeroes. The initial
allocation then happens when you add the first key-value-pair.
This will give your map an initial capacity of 128 items. If it runs out of space it will grow again.
Deallocating maps
Just like with dynamic arrays, you'll need to manually deallocate the memory of the map:
delete(some_map)
If you know all possible keys, then instead of a map just make an enum and use an enumerated array. Enumerated arrays
are much faster. With an enumerated array it can look up what element it needs directly since the enum acts like an
index. When fetching values from a map using a key, it instead has to search using the key, which is considerably slower
than directly fetching using an index.
Custom iterators
If you need to iterate over a collection in a special way, then you can define your own iterator.
My_Iterator :: struct {
index: int,
data: []i32,
}
my_iterator :: proc(it: ^My_Iterator) -> (val: i32, idx: int, cond: bool) {
if cond = it.index < len(it.data); cond {
val = it.data[it.index]
idx = it.index
it.index += 1
}
return
}
data := make([]i32, 6)
it := make_my_iterator(data)
for val in my_iterator(&it) {
fmt.println(val)
}
This iterator doesn't do anything different than the standard for val in data {} iterator would do. We see that
my_iterator returns false in the return value cond when the iteration should stop. For each lap of the for val in
my_iterator(&it) { loop it is able to pick up where the last lap left off using the index of the iterator.
We can also get the second loop variable, in this case the index:
Here's a more complicated example. It has a struct called Slot . Slot has a 'used' bool. We want an iterator that can
iterate over a slice of slots but only consider those for which used is true . Here's how we can do that:
Slot :: struct {
important_value: int,
used: bool,
}
Slots_Iterator :: struct {
index: int,
data: []Slot,
}
slots_iterator :: proc(it: ^Slots_Iterator) -> (val: Slot, idx: int, cond: bool) {
cond = it.index < len(it.data)
for ; cond; cond = it.index < len(it.data) {
if !it.data[it.index].used {
it.index += 1
continue
}
val = it.data[it.index]
idx = it.index
it.index += 1
break
}
return
}
slots[10] = {
important_value = 7,
used = true,
}
it := make_slots_iterator(slots[:])
The program will only print 7 , despite the slots slice having 128 entries. This is due to only a single slot having used =
true .
We use a for loop inside slots_iterator to skip all slots that are unused, which can be compared to our previous
example that only had an if. Having a loop in there is fine since it always picks up from it.index , i.e. it doesn't loop the
whole slice every time, it only loops until it finds the next slice that has used == true
Furthermore, what you can see is that all you need to be able to write for val in some_iterator(&it) {} is a proc
some_iterator that returns a value, an index (or other second loop variable) and finally the condition that the iteration
should continue.
If you need a modifyable value in the loop, then you can make a second version of the iterator proc that returns a pointer
to a value instead of just a value:
slots_iterator :: proc(it: ^Slots_Iterator) -> (val: ^Slot, idx: int, cond: bool) {
// ...
}
Manual memory management 1
Copyright Karl Zylinski 2024. This is a draft chapter from my upcoming Odin book. As it
is a draft, it may change heavily before the book is finalized. This file may not be
redistributed.
Let's take a step back and talk about manual memory management and dynamic memory
from the ground up.
Just to set things up we'll start with discussing what the stack is, and how non-dynamically
allocated memory works. Then we will look at how dynamic allocations of both variables
and collections work. We will thereafter have a look at the context and how it relates to
allocators. We'll also look at how to make manual memory management easier and less
scary using tracking allocators and temp allocators. We'll round things off with some
additional good practices before doing a short summary.
print_number :: proc() {
number: i32
number = 42
fmt.println(number)
}
The variable number has to live somewhere. It is of type i32 which is a 32 bit integer, i.e. it
needs 4 bytes of memory. Each procedure has what is known as a stack frame. A stack frame
is some memory that is big enough to hold the the procedure's local variables, parameters
and information about where to store the return values. This means that declaring a
variable like number: int will store that variable's memory within the stack frame.
You do not need to deallocate any memory that lives on the stack. Each procedure that is
being executed has a stack frame. These stack frames make up what is referred to as the
stack. The top-most stack frame of the stack is the current procedure, the one just below it
in the stack is the procedure that called the current procedure, etc etc all the way down to
the main procedure. When a procedure finishes, its stack frame is destroyed, or "popped"
off the top of the stack. Since all the local variables of a procedure live within the stack
frame, there's no need for any kind of deallocation of those variables.
Dynamically allocated variables 1.1.2 I say "probably" here because
you could in theory have an
allocator that allocates into
In contrast to stack allocated variables, it is possible allocate the memory of variables using
some stack memory. But that's
an allocator, which will probably make that variable's memory live outside the stack. You do
not the common case. For
this using new : simplicity, I will from here on
just assume that memory
number := new(i32) allocated using new does not
live on the stack.
new will by default use context.allocator to allocate memory. I.e. it implicitly does this:
The default value of context.allocator is on most platforms a heap allocator. new will
ask this allocator for 4 bytes of memory, because that's the size of a 32 bit integer. The heap
allocator will in turn ask the operating system for that amount of memory.
In this case number will be of type ^i32 . I.e. it is a pointer to a 32 bit integer. The memory
that the pointer points to is the 4 bytes of memory that was allocated for us.
Since this memory is not part of the stack frame, it will live on after the current procedure
free also takes an allocator of
finishes. You'll need to manually deallocate it in order for the memory to be returned to the default value
operating system. For anything you allocate using new , you deallocate it using free : context.allocator . If your
call to new used some custom
free(number) allocator that is different from
context.allocator , then
you'll need to pass the same
If you allocate the memory once on program startup, then the free isn't really necessary. A
allocator here.
program that shuts down has all its allocations freed automatically. But if you do new every
now and then as part of some recurring event in your program, then your program's
memory usage will grow and grow. This is what's known as a memory leak.
Within a procedure, this pointer lives on the stack, but the thing it points to does not. In
other words, these two variables
number := new(i32)
number2 := number
are both pointers. The pointers themselves are local stack variables. They both point to the
same memory. So if you do
number := new(i32)
number2 := number
number^ = 7
If you did number = 7 then it
number and number2 now point to an integer that has the value 7. The ^ in
then both
would not compile, because
number^ = 7 goes via the address that number holds and modifies the value at that you are trying to change the
address. pointer to point to some
random memory address 7.
Dynamically allocated collections 1.1.3
We have previously seen how to allocate and deallocate slices, dynamic arrays and maps.
Let's look at this again, but dig a bit deeper.
Say that you declare a dynamic array and append one item to it:
my_numbers: [dynamic]int
append(&my_numbers, 5)
Memory will be allocated when append runs, since the dynamic array must grow. This
works in the same manner as when we called new(int) previously, except that append
internally figures out how many bytes of memory it needs for growing the array, and asks
the allocator for that amount. Just like with new , append has an allocator parameter that
defaults to context.allocator . If append needs to grow a
dynamic array that already has
In order to deallocate the memory, use delete : some memory allocated, then
it will first allocate some new
delete(my_numbers) memory, copy over the
contents of the array to the
new memory and then
We've also seen the proc make , which we can use to pre-allocate dynamic arrays and
deallocate the old memory.
allocate slices:
delete(my_floats)
However, dynamic arrays, slices and maps are more complicated objects. They are
internally represented by structs that in turn contain a pointer to some allocated memory.
Therefore these are deallocated using delete . I.e. anything that was allocated using make
or append is deallocated using delete . In fact, delete is an explicit overload that looks
like this:
delete :: proc{
delete_string,
delete_cstring,
delete_dynamic_array,
delete_slice,
delete_map,
delete_soa_slice,
delete_soa_dynamic_array,
}
allocator: Allocator,
temp_allocator: Allocator,
assertion_failure_proc: Assertion_Failure_Proc,
logger: Logger,
random_generator: Random_Generator,
user_ptr: rawptr,
user_index: int,
// Internal use only
_internal: rawptr,
}
We've talked about how procs like make can be fed a custom allocator, i.e. something like
this:
Here we use context.temp_allocator , I will talk more about that allocator shortly. It is
an allocator that can be used for temporary things that are not needed in the long run. Here
I just use it to give an example of how to use something else than the default
context.allocator . However, since make 's allocator parameter will default to
context.allocator , so we could also do this:
context.allocator = context.temp_allocator
ints := make([]int, 4096)
I.e. override the current value of context.allocator . Since context is passed on to make
automatically, then make will use context.temp_allocator .
do_work :: proc() {
make_lots_of_ints :: proc() -> []int {
ints := make([]int, 4096)
return ints
}
context.allocator = context.temp_allocator
my_ints := make_lots_of_ints()
The context will be re-set to its old value when a scope ends. So context.allocator will
be restored to its former value at the end of do_work .
Also note that I left out the delete(my_ints) line on purpose here. As we shall discuss in
the chapter on temp allocators, that allocator doesn't need indivual deallocations.
do_work :: proc() {
make_lots_of_ints :: proc(allocator := context.allocator) -> []int {
ints := make([]int, 4096, allocator)
return ints
}
my_ints := make_lots_of_ints(context.temp_allocator)
do_work :: proc() {
make_lots_of_ints :: proc(allocator := context.allocator) -> []int {
context.allocator = allocator
ints := make([]int, 4096, allocator)
return ints
}
my_ints := make_lots_of_ints(context.temp_allocator)
I.e. since we want all allocations that happen within make_lots_of_ints to use the
parameter allocator , then I can just set context.allocator = allocator . Remember,
thecontext is restored to its former value when the current scope ends. So setting
context.allocator inside make_lots_of_ints only affects make_lots_of_ints and
any procedures it calls.
1. When you call a proc that allocates memory using context.allocator and you want to
use some other allocator, but it doesn't have any allocator parameter that you can
override.
2. When I really want all the code within a scope to use that allocator. For example, I tend to
setup what's known as a tracking allocator at the start of the program. In that case I say at
the top of my main proc that context.allocator should be my tracking allocator, which
makes my whole program use that allocator.
However, if you allocate memory periodically during the execution of your program, and
programs and video games that
always forget to deallocate it, then your program's memory usage will continuously grow.
There
will are somecrash
eventually commercial
due to
Then it's leaking for real!
memory leaks given that you
just let them run for long
enough.
To find memory leaks, I would advice that you use a tracking allocator. Odin comes with a
built in tracking allocator in the core:mem package. Below I show how to set it up (this
example code is from the overview):
package main
import "core:fmt"
import "core:mem"
main :: proc() {
when ODIN_DEBUG {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
defer {
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed: ===\n", len(track.alloc
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees: ===\n", len(track.bad_free_ar
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
}
rest_of_your_program()
}
So now, whenever any allocation happens using context.allocator (for example, when a
dynamic array tries to grow as part of append being called), then two things happen:
1. The allocation is done like usual using the wrapped allocator (also known as the backing
alloactor).
2. The source code location at which the allocation was done is recorded inside the
allocation_map that lives inside mem.Tracking_Allocator .
The code inside the defer block is what warns you about leaks when the program shuts
down. I've shown how to use defer before to defer single lines of code, but as you see here Bad frees often happen because
we can also defer a whole block. That whole block of code will run at the end of main . The you try to deallocate memory
code in there will check if anything is still left in the track.allocation_map . For each that has already been
thing left in there, it will print how many bytes and at what code location the leak deallocated. This can happen
when you have two systems
happened. The code in the defer block will also write warnings if you did any 'bad frees',
that both try to deallocate the
which means trying to free memory that wasn't actually allocated. Bad frees are as we see
same memory. Also, note that
stored on the bad_free_array field of the tracking allocator. it is OK to free nil pointers
and delete zeroed containers.
As you see we wrap the whole allocator setup and the defer in a when block: So some bad frees can be
avoided by zeroing out pointers
and containers after
when ODIN_DEBUG {
deallocating them.
The code within a when block will only be included in the program if the condition is true. If
the condition is false, then the code within the block will not be part of the program, at all.
In this case the condition is that ODIN_DEBUG is true. That constant is true if you run the Coming from C / C++
compiler with the -debug flag set. So in non-debug mode the tracking allocator will not be
when ODIN_DEBUG { is similar
set up, which is probably what you want, as tracking allocators slows the program down a
#if
to doing
bit. defined(ODIN_DEBUG) in C.
There's a gotcha here: The when ODIN_DEBUG {} block looks like a scope. So wouldn't the
defer happen at the end of that scope, before even reaching the
rest_of_your_program() ? The answer is no. The curly braces associated with a when
block do not set up a real "scope", it is only a block of code. defer does not care about
when the when block ends and variables declared within a when block exist after the block
ends.
Unless you get noticeably bad performance from using the tracking allocator, I advice you
to run with the tracking allocator on all the time in the 'development build' of your
program. Disable it when you send out releases to users, which will happen automatically
since those release builds should not use the -debug compiler flag.
By using a tracking allocator I hope that you'll find dynamic memory allocations less scary.
You'll be able to look at the reports of where memory leaked in order to learn more about
when memory is actually allocated.
Finally, if you are still confused by anything, then you can watch this video I made on the
tracking allocator:
For example, say that you need to create a string by combining a few different strings and
some variables and immediately show it on the screen:
Here the a in aprint means allocate . That proc will combine the parameters you feed it
and construct a string from them. By default it allocates the string using
context.allocator . The string is sent to show_player_info in order to show it on the
screen. Afterwards you also delete the string so that you don't leak memory.
the tprint proc always uses context.temp_allocator . This is identical to the previous
example, but shorter.
You can also use the temp allocator to create temporary dynamic arrays. This is useful when
you have an algorithm that needs to do some processing using a dynamic array, but it
doesn't need the dynamic array once the proc is done:
for i in 0..<100 {
append(&numbers, i)
}
rand.shuffle(numbers[:])
return numbers[0]
}
This is of course a silly example, but it shows that you do not need to delete the dynamic
array before returning, since it used the temp allocator.
Level :: struct {
name: string,
objects: []Game_Object,
tiles: []Tiles,
}
Assume that Game_Objects and Tiles are structs that have some kind of position and
some data of how they look etc. That part is not important. Now, say that you have a
procedure that generates the level:
Here generate_tiles and generate_objects are two procs that do some procedural
generation, they return a slice each that is allocated using context.allocator . Assume
that those two procs have an optional parameter allocator := context.allocator . So
Level then contains three separately allocated things: A clone of the string name , the
tiles slice and the objects slice.
This might not seem too bad in this case, but sometimes this kind of level destroying can
get quite intense. Say that each object also has a name string that is separately cloned:
What you can do to free all this memory with a single procedure call, is to use a memory
arena. A memory arena can be used to allocate memory into a specific region of memory,
meaning that you can deallocate that whole region of memory all at once. Here we will use
the growing virtual memory arena. I'll explain how it works under the hood later, but let's
first look at the code.
Level :: struct {
name: string,
objects: []Game_Object,
tiles: []Tiles,
arena: vmem.Arena,
}
tiles := generate_tiles(arena_allocator)
objects := generate_objects(tiles, arena_allocator)
return {
name = strings.clone(name, arena_allocator),
objects = objects,
tiles = tiles,
arena = level_arena,
}
}
Note that we also assign arena = level_arena when we return the Level struct.
As you can see the destroy_level proc is now very simple. It just destroys the level's
arena. This will go through all the arena's memory blocks and deallocate them all. Since all
the memory of the name , objects and tiles fields were allocated into that arena, all of
them are now deallocated.
A good take-away from all this is: Arenas are great for grouping allocations that have the
same lifetime. Meaning that if you need several things to be deallocated at the same time,
then perhaps using an arena to allocate them is a good idea. If they have different lifetimes
then you are perhaps trying to put unrelated things into an arena.
Please note that since arenas use blocks of memory, that means you can't deallocate
individual allocations, you can only destroy the entire arena. delete(level.name) doesn't
really do anything, it will just return a "Not Implemented" error. However, you can still
keep "lists of free slots" for arena-allocated arrays and thus be able to reuse those slots.
I have never tried to combine arenas with tracking allocators. I.e. I don't get any help with
remembering to destroy my arenas. Since there are often few enough of them, it means that
it's not a big issue.
1. One in core:mem/virtual
2. One in core:mem
I'm here to tell you to only use the one in core:mem/virtual . The only time you'll need the
one in core:mem is if you are on platforms that do not support virtual memory, such as
WASM. In fact, the one in core:mem/virtual can do the same stuff as the one in
core:mem , if you configure it properly, just that it won't compile on platforms without
virtual memory.
So "looking at the different types arena allocactors" actually means that we are gonna look
at the three different modes of operation of the virtual memory arena allocator. The modes
of operation are governed by this enum:
As we can see Growing is equivalent to 0 . That's why you get a growing virtual memory
arena when you type just arena: vmem.Arena , since the kind: Arena_Kind field in the
Arena struct is then zero-initialized.
arena: vmem.Arena
arena_allocator := vmem.arena_allocator(&arena)
arena: vmem.Arena
err := vmem.arena_init_growing(&arena) arena_init_growing will set
assert(err == .None) thekind field in arena to
arena_allocator := vmem.arena_allocator(&arena) .Growing .
Note that arena_init_growing returns an error, we assert that it has the value .None ,
meaning that we treat it as a fatal error if the creation of our arena failed.
arena: vmem.Arena
err := vmem.arena_init_growing(&arena, 100*mem.Megabyte)
assert(err == .None) The mem.Megabyte constant
arena_allocator := vmem.arena_allocator(&arena) lives in core:mem
This arena consists of blocks where each block knows the location of the previous block.
When allocations happen those allocations go into these blocks. When the current block is
full then a new block is reserved. Since each block knows the location of the previous block,
then that means that vmem.arena_destroy(&arena) can walk through all those blocks
and deallocate them all.
Note the word reserved above. This arena uses virtual memory. Virtual memory allocations
can happen in two steps: First you can reserve memory, which just tells the operating
system that you want X bytes of continous virtual memory. No physical memory is
allocated at this step. I.e. the memory usage of the program does not go up when you
reserve virtual memory. It is only when you commit already reserved memory that physical
memory is allocated, and you can commit it in small chunks.
What this means is that the blocks of memory that the arena uses are initially just reserved.
Then for each allocation it may commit parts of that reserved memory. So even though we
have nice continous chunks of virtual memory ready, the program's memory usage will
only go up when the allocations actually happen. The virtual memory arena will by default
reserve blocks of 1 megabyte, however if you try to allocate something really big then it will
reserve a big enough block to accomodate your allocation.
vmem.arena_destroy(&arena)
which frees all the allocations made using this arena in one go.
arena: vmem.Arena
err := vmem.arena_init_static(&arena, 1*mem.Gigabyte)
assert(err == .None) the kind field of arena have
arena_allocator := vmem.arena_allocator(&arena) the value .Static .
A static virtual memory arena also uses virtual memory in the sense that it allocates a block
of virtual memory and then commits parts of it as you do your allocations. The difference
from the growing one is that this one will never create new blocks. The block size you tell it
to use at the start will be all the memory it has. Above we reserve a big 1 Gigabyte block.
Just like with the growing virtual arena, this arena only does a virtual reservation of the
memory, the actual memory usage will only go up as the virtual memory is commited as
part of allocations being done using the allocator.
Just like with the growing arena, destroying this arena also frees up all the memory it
allocated:
vmem.arena_destroy(&arena)
arena: vmem.Arena
buf := make([]byte, 10*mem.Megabyte)
err := vmem.arena_init_buffer(&arena, buf[:])
assert(err == .None) the kind of arena have the
arena_allocator := vmem.arena_allocator(&arena) value .Buffer .
The fixed buffer arena does not use virtual memory at all, you just feed it a buffer of bytes
and it uses that as the memory for the arena. The above gives you a 10 megabyte buffer that
is allocated up-front. The allocations will go into that buffer.
To destroy your arena, yo do not need to call vmem.arena_destroy , the only allocation
happened on the make line. So instead you deallocate this one using
delete(buf)
The fixed buffer mode of this core:mem/virtual arena works mostly like the arena
allocator you find in core:mem . So you do not need to bother with the one in core:mem ,
unless you are on a platform that doesn't support virtual memory, in which case the
core:mem/virtual package won't compile.
A funny thing you can do with the buffer arena is to allocate into stack memory:
arena: vmem.Arena
buf: [4096]byte
err := vmem.arena_init_buffer(&arena, buf[:])
assert(err == .None)
arena_allocator := vmem.arena_allocator(&arena)
Since the buffer buf is allocated on the stack this means that no dynamic memory
allocations are done at all when you use this arena allocator. However, stack space is
limited, using anything bigger than 10000 or 100000 bytes for the buffer is proabably a
bad idea as the program will crash if you run out of stack space. That limits the usefulness of
doing this.
Person :: struct {
health: int,
age: int,
}
people: [dynamic]^Person
for i in 0..<10000 {
append(&people, make_random_person())
}
This code creates a dynamic array that of pointers to Person structs. Inside
make_random_person we use new to separately allocate each person and then we set their
health and age to random values between 0 and 100 . If we use the default allocator here,
which is a heap allocator, then each Person struct will end up att different places in
memory. We create 10000 such people and add all of them to the people dynamic array.
Say that we later iterate people and print their health and age:
for p in people {
fmt.println(p.health, p.age)
}
Here the loop variable p is of type ^Person . Since the memory those pointers refer to are
separately heap allocated they can be anywhere in the computer's memory. For each lap of
the loop it is jumping around in memory as it fetches p.health and p.age for different
pointers p . This is very inefficient. Why? Because the computer has a thing called a cache.
Whenever it fetches for example p.health it also fetches some memory in the region
These caches are fast because
around p.health and puts that stuff in a very fast (but very small) cache. During the next they are literally near the CPU.
lap of the loop, since we use separate heap allocations, p won't (probably) be near the There are several layers of
caches called L1, L2 and L3,
previous p , so instead of being able to reuse the stuff in the cache, it has to fetch
where a lower number means a
completely new stuff. This is known as a cache miss. Fetching stuff outside the cache is
smaller cache, but closer to the
much slower than reusing the cache. CPU, therefore faster. If your
memory isn't in any of the
How do you avoid these cache misses? The first step is to avoid separately allocating things, caches, then it has to go to the
especially many small things in an array. Here's a more cache friendly version: normal computer memory,
which is much slower.
Person :: struct {
health: int,
age: int,
}
people: [dynamic]Person
for i in 0..<10000 {
append(&people, make_random_person())
}
Note how the ^ are gone in front of Person and I no longer do new . Now people is just a
continous array of memory that contains objects of type Person one after the other.
for p in people {
fmt.println(p.health, p.age)
} Coming from C / C++
If you've used C++ with lots of
Then this loop will run much faster than it did before. p is very likely to already be in the inheritance, then you are
cache (except for the first lap of the loop). You'll only get a cache miss after a bunch of essentially forced to do these
iterations, i.e. when you land on a p that is outside what the cache currently holds. With the separate allocations. You'll be
used to seeing stuff like
old code you got a cache miss almost every iteration.
std::vector<Person*> etc.
You cannot just keep plain
Now, this example alone won't make much of a difference, but if you try to program this
Person values in there
way in general, then your programs will be much faster. because inheritance requires
separate allocations, due to the
Problem: Pointers get invalidated when array grows varying size memory size of
each item. This is one reason
As I mentioned in a previous chapter, when a dynamic array grows all the memory gets why many people today
moved to a new place. consider object-oriented
programming troublesome.
So if you for example store a pointer to one of the items in the people array (the fast one)
discussed above:
p := &people[10]
and you keep that pointer around while you also append more items to that array, then the
pointer might suddenly be invalid due to the growth of the array.
To counter this I recommend the following: Whenever you need to store a reference to
something in an array for a long period of time, then use handles instead of pointers. These
handles are essentially the index of the array elements plus some info regarding if the item
at that index has been reused. See the handle-based array example.
Shallow copying 1.3.2
Whenever you copy the value of a struct to a separate variable, then all the fields get copied
over. But if one of those fields are a pointer, then it just copies the pointer itself. The
memory the pointer refers to is not cloned. You'll have to do that manually whenever you
need it to happen.
One example of this is when you copy dynamic arrays. A dynamic array looks like this
internally:
Raw_Dynamic_Array :: struct {
data: rawptr,
len: int,
cap: int,
allocator: Allocator,
}
dyn_arr: [dynamic]int
append(&dyn_arr, 5)
then dyn_arr is a struct that lives on the stack, but it has a pointer data that points to
dynamically allocated memory.
dyn_arr: [dynamic]int
append(&dyn_arr, 5)
dyn_arr_2 := dyn_arr
then dyn_arr_2 is a copy of dyn_arr , but the data field in both contains the same
pointer, i.e. points to the same memory. After that there is a loop that adds things to
dyn_arr , which will make it grow. This will allocate new memory for dyn_arr , update the
data pointer and then deallocate whatever was at the old data pointer. But since
dyn_arr_2 is just a copy of however dyn_arr looked before the loop, it will not be updated
with the new data pointer. In fact, the data in dyn_arr_2 will point to invalid memory.
import "core:slice"
dyn_arr_2 := slice.clone_to_dynamic(dyn_arr[:])
to get a clone with its own memory. Now dyn_arr and dyn_arr_2 work independently. In
case the clone doesn't need to be a dynamic array, then there is also this proc:
s := slice.clone(dyn_arr[:])
which will just return a slice containing a clone of the data in the dynamic array.
Earlier I talked about avoiding doing many small heap allocations. If you avoid that, then
itch version comes with Odin
most of your remaining dynamic memory allocations might go into allocating different source code.
forms of arrays. What you can then do is to start using only fixed arrays instead of those
dynamic arrays.
package main
import "core:log"
MAX_NUMBERS :: 4096
numbers: [MAX_NUMBERS]int
num_numbers: int
numbers[num_numbers] = n
num_numbers += 1
}
main :: proc() {
add_number(7)
add_number(42)
Here you can see that we have a fixed array of 4096 integers that we can add items to.
add_numbers puts a new integer at the end of the list and then increments the
num_numbers counter, which is used for keeping track of how much of the fixed array
we've actually used.
Now, the code above is simple, but you probably want functionality for removing items etc.
Fortunately, there is an array that implements all this in core! Here's how you can change
the example above to use it:
package main
import sa "core:container/small_array"
MAX_NUMBERS :: 4096
numbers: sa.Small_Array(MAX_NUMBERS, int)
main :: proc() {
sa.append(&numbers, 7)
sa.append(&numbers, 5)
sa.append(&numbers, 42)
Ignore the strange name Small_Array ; these arrays can be as big as any fixed global array.
The struct Small_Array actually looks like this:
Finally, one thing you can do to force yourself to work like this is to set the default allocator
to the panic allocator:
import "base:runtime"
context.allocator = runtime.panic_allocator()
Now when any memory allocation using context.allocator happens, then your program
crashes on purpose. You'll be able to see the line it crashed on, i.e. where the allocation
happened.
The answer is: There is no consensus on any "right way". I've have shown you how to track
memory leaks, how to use the temp allocator, how arena allocators work as well as some
additional good practices. If you learn to use these tools and grow your understanding, then
all of this will quickly become much easier. Let's end this chapter by summarzing our
findings into two coheasive "strategies".
You can also use arena allocators as a compliment. In the example I showed earlier I had a
few seprately allocated arrays and strings that all had the same lifetime, i.e. they should be
destroyed together. In such a case you can use an arena to group the allocations and later
just destroy the whole arena instad of doing several separate deallocations.
How to pass parameters of any type to a procedure, i.e. generic procedure parameters
How to pass compile-time constants to a procedure
How to create structs that contain fields of a generic type
Throughout this chapter you'll see $ appearing a lot. Sometimes it will be part of a proc
parameter type, sometimes it will be part of a proc parameter name, sometimes both. It'll
also appear on struct defintitions. Make sure you pay attention to where the $ is in the
following examples.
This system of generics that involve using the $ is known as parametric polymorphism
(parapoly). In other langauges polymorphism usually means that we use a single symbol to
denote multiple types. As you will see, Odin's parapoly has a slightly wider definition than
that. You will see the same system be used for both passing parameters of generic types but
also for enforcing parameters to be compile-time values.
return val
}
We might want clamp to work with int and f64 numbers as well. Here's how we make
clamp work with a generic parameter:
The f32 parameter type has been replaced with a $T . This means that the type of val , min
and max (they all have to be the same type) will be usable by typing T . As you can see we
use T as the type of the return value of the proc.
What actually happens here? When the compiler runs and sees that your code uses clamp
with a value of a specific type, then it will generate a version of clamp where it replaces T
with the type of the value you are feeding into it. In other words, when we do something
like this:
var: int = 7
clamped_var := clamp(var, 2, 5)
Note that we only type $T with a dollar sign once. I.e. on the return value we just type T .
When we put the dollar sign on the type of a parameter we are effectively saying "I want to
create a compile-time constant T based on the type of this parameter". Note that I say
"compile-time constant" here. Everything with a $ in front is a compile-time constant,
this is because the compiler uses those things to generate code at compile-time, so it must
be known at compile-time.
If it is not possible to generate the code for a certain type, then you'll get a compilation
error. In the case of our clamp example, it could happen if you try to use this proc with
types that don't allow you to use <= and >= .
To only allow this proc to be used with numeric types, you can alter the declaration of You may ask "but doesn't using
clamp this like: <= and >= already implicitly
require a numeric type?". No,
clamp :: proc(val, min, max: $T) -> T where intrinsics.type_is_numeric(T) there are other types that
implement those operators,
such as strings.
Here we demand that T be a numeric type. This uses core:instrinsics , which you'll have
to import. The condition of the where must be possible to evaluate at compile-time. The
code in intrinsics contains special 'intrinsic procedures' that are defined by the compiler
and thus available at compile-time.
my_slice := make_random_sized_slice(f32)
my_slice will then be a slice of type []f32 with a size between 0 and 1024 ( 1024 because
we calculate the size of the allocated slice using random_size :=
rand.int31_max(1024) , which gives us a random value in the range 0 to 1024). Note the
parameter when we call the proc: my_slice := make_random_sized_slice(f32) . It is
just a type!
The difference between this and the clamp example is that in this one I moved the $ to the
parameter name instead of having it on the type. I.e. I wrote $T: typeid while in the
clamp proc did val, min, max: $T . What is this typeid thing we use here? It is the
"type of types", or rather, the unique identifier for each type. We can then use T in the code
to refer to this type.
Note that we cannot just type T: typeid in the parameter list, the $ has to be there. Again,
the $ means that T is a compile-time constant, which makes it possible for the compiler to
reason about it while it is compiling the program. It needs to be able to do so since it is
generating the different versions of make_random_sized_slice based on the type we send
in.
Furthermore, one important thing to realize is that the T in $T: typeid of this proc and
the T in val, min, max: $T are both typeids. In the clamp case the typeid T is figured
out implicitly from the parameter passed and in the make_random_sized_slice case we
explicitly pass a parameter into T . So $T: typeid can be used whenever you want to
reason about a type, but got no value. But if you got a value and want to use the value and
also reason about it's type, then you can do something like val: $T .
We can pass compile-time constants of other types than typeid , a common example is to
pass a compile-time constant integer. Here's a procedure that creates a fixed array of size
N and sets all the elements to 7 :
We see that this struct has a list of parameters (usually you just type Some_Name ::
struct { ). The first parameter is $T: typeid , which is used to choose the type of the
items array. The second parameter is $N: int , which gives us a compile-time value to
use for the size of the items array.
array will then be a variable that is of the Special_Array type. The field items inside
array will contain 128 items of type f64 . You can see it as the compiler taking then f64
and the 128 and "pasting" it into the struct whereever T and N are used, i.e. it is
generating variations of this struct type for you.
Also, here's how you can can write procedures that can accept these generic structs as
parameters:
We see that this proc can figure out the type T it needs for the return value from the
Special_Array($T, $N) in the parameter list. It will know what those T and N are based
on the specific type of Special_Array we sent in.
Note that when creating structs you can only have the $ on the parameter name. Doing
something like val: $T within a struct declaration doesn't make sense: The struct is a
type, so we cannot feed the declaration of the struct a run-time value such as val .
val: $T
$T: typeid
$N: int
In all these cases the thing with the $ in front is a compile-time constant.
You may find it strange that all these things fall under the name parametric polymorphism.
Some people might argue that only the val: $T example really is polymorphism. But the
name aside, it is important to understand that all these things fall under the same system.
And in a nutshell the we can define this system like so:
Parametric polymorphism is the system by which the compiler looks at all the $ -decorated
parameters within a proc/struct and uses them as a template to generate variations of the
proc/struct. One variation will be generated for each unique combination of passed
polymorphic parameters.
Specialization 1.5
Let's finish this chapter by talking about how you can limit what types that are allowed
when working with parametric polymorphism. This is known as specialization. Let's look at
a few examples.
In this case we are enforcing that this polymorphic parameter must be a dynamic array.
However, note the $E at the end of [dynamic]$E . This means that we allow the dynamic
array to have elements of any type. Different variations of delete_dynamic_array will be
generated when you use it with dynamic arrays that have elements of different types. It's
possible to use type E within the proc, which it does like so: cap(array)*size_of(E) .
This proc makes slices of a certain type. But note the difference between this and
delete_dynamic_array ! Here we are just passing a $T: typeid , but we are specializing
the typeid itself: The $ says that T must be a compile-time constant typeid, but we
enforce that the typeid is a slice with arbitrary element type E . This might seem strange:
From the previous example you might assume that / only works with polymorphic
parameters (i.e. something like array: $T/[dynamic]$E ). But we can actually specialize
both polymorphic types such as $T and also compile-time constant parameter of type
typeid . This actually makes sense, since I said earlier that when you type val: $T , i.e. the
$ being on the type, then T is actaully a typeid . So in both cases we are specializing a
typeid, just that in the array: $T/[dynamic]$E case we also have a value of that type.
number := 7
number_pointer := &number
increment_number(number_pointer)
fmt.println(number) // prints 8
number is just a variable of type int . Note the & in the line
number_pointer := &number
The & fetches the address of number . This means that the variable number_pointer now
contains that address. This address tells us where in the computer's memory number is
stored. We can use that address to access and modify number from other parts of our code.
The type of number_pointer is ^int . Any type that contains an ^ is a pointer. A pointer is
something that can store an address to something else. I.e. it "points to some part of
memory". ^int can be read as "pointer to integer", meaning that we expect this pointer to
contain the memory address of an integer.
The line num^ += 1 fetches the integer at the address that num points to, adds 1 to it and
stores it back at that address. Note the position of the ^ . It is to the right of the parameter
name. By putting ^ to the right a pointer's name you are able to both fetch and set the value
at that address.
Note that when we wrote the type name ^int , then ^ was on the left. It's always to the left
when making types that are pointers. It's always to the right of when we are fetching the
value that a pointer contains.
num^ += 1
}
Cat :: struct {
name: string,
age: int,
color: Cat_Color,
}
Cat_Color :: enum {
Black,
White,
Orange,
Tabby,
Calico,
}
It's the cat's birthday so we need to increment the age and print a happy message:
cat.age += 1
fmt.printfln("Hooray, %v is now %v years old!", cat.name, cat.age)
}
my_cat := Cat {
name = "Patches",
age = 7,
color = .Orange,
}
process_cat_birthday(&my_cat)
Just like previously we fetch the address of something using the &:
process_cat_birthday(&my_cat)
The result of &my_cat is a something of type ^Cat , i.e. a pointer to a struct of type Cat .
cat.age += 1
On the other hand, if you want replace the whole struct, then you need to use ^: the same thing as -> does in C.
cat^ = {
name = "Klucke"
age = 6,
color = .Tabby,
}
}
The reason you can't just do cat = { is that assigning to a pointer means changing the
address the pointer contains, i.e. assigning to a pointer is for re-directing the pointer to
point to something else. So you have to do include the ^ if you want to go via the pointer
and replace the thing it points to.
number := 7
pointer1 := &number
pointer2 := pointer1
pointer2^ = 10
Then just think of pointer1 and pointer2 as two variables containing the exact same
number, i.e. they contain the same address. To modify the original variable number , you
can go via any of the two pointers: pointer1^ = 10 and pointer2^ = 10 would both set
the variable number to 10 .
However, since pointer1 and pointer2 are two separate variables, they must both store a
separate copy of the address somewhere. So if you took the address of pointer1 and
pointer2 and printed it, then you would see two different addresses:
fmt.println(&pointer1)
fmt.println(&pointer2)
The type of of &pointer1 is in this case ^^int , which you can read as "pointer to a pointer
Pointers are by default printed
to an integer". The above would print something like
using hexadecimal notation. If
you rather look at "normal"
0x445C6FF868 numbers, then you can change
0x445C6FF860 the print lines to:
fmt.printfln("%i",
The exact numbers will be different on your computer. Just note that in my case the first &pointer1) .
one ends with 8 and the second one ends with 0 .
This shows that these pointers are variables just like any other variable. They have a
location in memory where they store their data. That location has an address. But at the
address of our two pointers pointer1 and pointer2 we find the exact same value: The
address of the integer variable number .
number_pointer^ = 10
At a surface level, it seems like when we find number_pointer^ on the left side of an = ,
then we go via the pointer and set the value at that address:
number_pointer^ = 10
And also at a surface level, it seems like when we find number_pointer^ on the right of :=
(or on the right of = ), then we fetch the number the pointer refers to:
another_number := number_pointer^
However, if we talk about what the compiler is actually doing, then we say that
number_pointer^ , the whole thing including the ^ , is an L-value. L-values are
addressable, meaning that they are possible to assign to. You can also fetch their value.
So on a line like
number_pointer^ = 10
we write into the L-value number_pointer^ because it is on the left side of the
assignment. But on a line like
another_number := number_pointer^
then the L-value is read because it is on the right side of the assignment. In the second case
another_number is also an L-value, because we could assign to it. To reduce the confusion, some
people re-brand L-value to
"Locator-value", because it
L-value is short for "Left-value". Historically (as in programming history in general), L-
can locate the data when you
values were only on the left side, which is no longer true. Nowadays it just means that they
assign to it. That sounds about
can appear on the left side. equally confusing to me
though. Some people skip all of
Since there are Left-values, then there are also Right-values. A number like 7 is an R- this and just say addressable
value, you can never assign to it, because doing instead.
7 = some_variable
doesn't make any sense. Again the name is supposed to hint that they can only appear on
the right side in an assignment, which in this case is true.
I like to think of L-values as the compiler's own internal version of pointers. Meaning that
when we write number_pointer^ then the compiler still retains the address of the
whatever number_pointer points to, so you can assign to it.
The existence of L-values is also why it's possible to take the address of a value you just
fetched from an array:
array: [20]int
element_pointer := &array[10]
If you have an array and fetch the element at index 10 using array[10] , then you might
think that you've already completely lost the original memory address of that value. But in
the example above element_pointer somehow contains the direct address to the tenth
element in the array. Somehow the & is still able to figure out this address, despite it
looking like array[10] has already fetched the value! This is because array[10] is an L-
value. Taking the address of an L-value gives you back the "original address". This can be
compared to doing this:
array: [20]int
element := array[10]
element_pointer := &element
Here we assign the value of array[10] to a new variable element . In doing so array[10]
is read and we forget where it came from. Therefore element_pointer just points to the
new variable element instead.
As a final example, if you dereference a pointer and immediately take the address of it
again, then you get the original pointer back:
number := 7
number_pointer := &number
number_pointer_again := &number_pointer^
Both number_pointer and number_pointer_again are pointers that contain the address
of number . As long as the L-value hasn't been read and the underlying value crossed over
the = , then you still have one last chance to fetch its original address.