Building Blocks Of Zig: Understanding Structs
Tags: zig, programming, softwaredevelopment
Published: 2024-05-29 09:41:36


To learn more about Zig and why I think it's an amazing language check out my blog post [Zig is The Next Big Programming Language](https://dayvster.com/blog/zig-is-the-next-big-language/)

## What is a Struct in Zig?
Like many other programming languages Zig has structs. A struct is a pretty simple and straightforward concept, it's a user defined data model that can contain multiple fields or members.
That's a lot of words to say that a struct is a way to group related data together in a logical and structured way.


Traditional OOP languages like C++, C# and Java use classes to achieve the same goal, we all know the old and tired analogies of classes representing a generic concept like animal and then subclasses like dog and cat inheriting from the animal class and adding their own specific behavior and data or implementing interfaces to achieve polymorphism.

Zig like C does not have classes but it does have **structs**, **enums** and **unions**. In Zig structs are the most common way to group related data together.

## How to define a Struct in Zig
In Zig you define a struct by using the `struct` keyword followed by the name of the struct and then the body of the struct enclosed in curly braces `{}`. Inside the body of the struct you define the fields or members of the struct.

Here is an example of a struct that represents a game character:

```zig
const Character = struct {
    name: []const u8,
    health: u32,
    stamina: u32,
    say_hello: fn ([]const u8) void,
};
```

Now that we have defined the `Character` struct we can create instances of it like this:

```zig
const player = Character{
    .name = "Ziggy Stardust",
    .health = 100,
    .stamina = 100,
    .say_hello = fn(name: []const u8) void {
        std.log.info("Hello, {s}!\n", .{name}),
    },
};

// Call the say_hello function
player.say_hello(player.name);

```

In this example we create a new instance of the `Character` struct called `player` and we initialize the fields of the struct with the values `"Ziggy Stardust"`, `100`, `100` and a function that logs a message to the console.

Super simple right? Fairly similar to how you would define a struct in C. But wait there's more
 
## Struct Fields can have default values
Default values in Zig Structs are executed at comptime(compile time) and are allow the field to be omitted when creating an instance of the struct.
This allows us to essentially have optional typesafe fields in our structs that have a default value if not provided.

For example we can modify the `Character` struct to have default values for the `health` and `stamina` fields:

```zig
const Character = struct {
    name: []const u8,
    health: u32 = 100,
    stamina: u32 = 100,
    say_hello: fn ([]const u8) void,
};
```

This way when we create a new instance of the `Character` struct we can omit the `health` and `stamina` fields and they will be initialized with the default values, which in this case will be `100` for both fields.

```zig
const player = Character{
    .name = "Ziggy Stardust",
    .say_hello = fn(name: []const u8) void {
        std.log.info("Hello, {s}!\n", .{name}),
    },
};
```

## Structs can be packed
By default structs in zig do not maintain a specific order of fields regardless of the order in which you define the fields in the struct. This is not always optimal since sometimes you may wanna have a specific order of your fields to optimize memory usage or interact with certain libraries like OpenGL that require a specific order of fields in a struct.
for this we can use the `packed` keyword when defining a struct to ensure that the fields are ordered in the same order as they are defined in the struct.

Additionally there will be no padding between the fields in a packed struct.

packed structs can participate in Bit Cast or Pointer Cast operations including during comptime.
  
```zig  
  const Character = packed struct {
    name: []const u8,
    health: u32,
    stamina: u32,
    say_hello: fn ([]const u8) void,
};
```

Now the bytes of the struct will be ordered in the same order as they are defined in the struct. Simple as that.

## Struct fields can be undefined
If you are not yet ready to set a value to a field in a zig struct, you can use the `undefined` keyword to set the field to an undefined state. This is useful when you want to create a struct with some fields that you will set later on in your code.

```zig
const Goblin = Character{
  .name = undefined,
  .health = 100,
  .stamina = 100,
  .say_hello = fn(name: []const u8) void {
    std.log.info("Hello, {s}!\n", .{name}),
  },
}
```
In this example we create a new instance of the `Character` struct called `Goblin` and we set the `name` field to `undefined` and the `health` and `stamina` fields to `100`. This way we can create a new instance of the `Goblin` struct and set the `name` field later on in our code, perhaps we wanna give the goblin a randomly generated name when the goblin enters the view of the player. 

## Struct can have Methods / Functions
You may have noticed that we have a function as a field in our `Character` struct. Zig like many other languages allows you to define functions as struct fields which can be called on instances of the struct.  
The `say_hello` field in the `Character` struct is a function that takes a `[]const u8` parameter and returns `void` meaning no value is returned.
But we could for example have a function that allows us to attack another character by passing in the character reference as a parameter.

```zig
const Character = struct {
    name: []const u8,
    health: u32,
    stamina: u32,
    say_hello: fn ([]const u8) void,
    attack: fn (target: *Character) void,
};

const player = Character{
    .name = "Ziggy Stardust",
    .health = 100,
    .stamina = 100,
    .say_hello = fn(name: []const u8) void {
        std.log.info("Hello, {s}!\n", .{name}),
    },
    .attack = fn(target: *Character) void {
        std.log.info("{s} attacks {s}!\n", .{player.name, target.name}),
    },
};

const enemy = Character{
    .name = "Goblin",
    .health = 50,
    .stamina = 50,
    .say_hello = fn(name: []const u8) void {
        std.log.info("Hello, {s}!\n", .{name}),
    },
    .attack = fn(target: *Character) void {
        std.log.info("{s} attacks {s}!\n", .{enemy.name, target.name}),
    },
};

player.attack(&enemy);
enemy.attack(&player);
```

In this example we have added an `attack` function to the `Character` struct that takes a pointer to another `Character` as a parameter and logs a message to the console. We then create two instances of the `Character` struct called `player` and `enemy` and call the `attack` function on each of them passing in the other character as a parameter.

Optimally we'd also want to add a `take_damage` function to the `Character` struct that would reduce the health of the character when attacked. But this blog post is purely educational.

## Structs can be returned from a function which results in a Generic
In Zig like many other languages you can return a struct from a function. But where it gets super interesting is how you can leverage that to create generics
Here 

```zig

fn GoblinHorde(comptime T: type) type {
  return struct {
    pub const Goblin = struct {
      prev: ?*Goblin,
      next: ?*Goblin,
      data: T,
    }
    first: ?*Goblin,
    last: ?*Goblin,
    len: usize,
  }
}
```

## Conclusion
Struct are a fundamental building block of Zig and are used to group related data together in a logical and structured way. Structs can have default values, be packed, have undefined fields, have methods and be returned from functions which results in a generic. Structs are a powerful and flexible way to model data in Zig and are used extensively in the standard library and in many third party libraries.