Skip to content
Chung Leong edited this page Jul 20, 2024 · 10 revisions

In the Zig language, a struct is a composite data type that hold multiple variables (referred to as "fields"). It's analogous to object in JavaScript. When a Zig struct is exported, it can be instantiated in JavaScript through its constructor. The constructor would expect an object containing initial values for its fields. Initializers can be omitted for fields with default values.

pub const User = struct {
    id: u64,
    name: []const u8,
    email: []const u8,
    age: ?u32 = null,
    popularity: i64 = -1,
};
import { User } from './struct-example-1.zig';

const user = new User({
    id: 1234n,
    name: "Bigus Dickus",
    email: "madeupname12@rome.gov.it",
});
console.log(user.id);
console.log(user.name.string);
console.log(user.email);
console.log(user.age);
console.log(user.popularity);
1234n
Bigus Dickus
madeupname12@rome.gov.it
null
-1n

Analogous to the syntax of Zig, you can assign an object literal to a struct field:

const std = @import("std");

pub const Address = struct {
    street: []const u8,
    city: []const u8,
    state: [2]u8,
    zipCode: u32,
};
pub const User = struct {
    id: u64,
    name: []const u8,
    email: []const u8,
    age: ?u32 = null,
    popularity: i64 = -1,
    address: ?Address,

    pub fn print(self: User) void {
        std.debug.print("Name: {s}\n", .{self.name});
        std.debug.print("E-mail: {s}\n", .{self.email});
        if (self.age) |age| {
            std.debug.print("Age: {d}\n", .{age});
        }
        std.debug.print("Popularity: {d}\n", .{self.popularity});
        if (self.address) |address| {
            std.debug.print("Street: {s}\n", .{address.street});
            std.debug.print("City: {s}\n", .{address.city});
            std.debug.print("State: {s}\n", .{address.state});
            std.debug.print("ZIP code: {d}\n", .{address.zipCode});
        }
    }
};
import { User } from './struct-example-2.zig';

const user = new User({
    id: 1234n,
    name: "Bigus Dickus",
    email: "madeupname12@rome.gov.it",
    age: 32,
    address: null,
});
user.address = {
    street: '1 Colosseum Sq.',
    city: 'Rome',
    state: 'NY',
    zipCode: '10001',
};
user.print();
Name: Bigus Dickus
E-mail: madeupname12@rome.gov.it
Age: 32
Popularity: -1
Street: 1 Colosseum Sq.
City: Rome
State: NY
ZIP code: 10001

You can also pass an object literal to a function expecting a struct as argument:

import { User } from './struct-example-2.zig';

User.print({
    id: 1234n,
    name: "Bigus Dickus",
    email: "madeupname12@rome.gov.it",
    age: 32,
    address: {
        street: '1 Colosseum Sq.',
        city: 'Rome',
        state: 'NY',
        zipCode: '10001',
    },
});

Iterating through struct fields

Struct fields are non-enumerable. Passing a struct to Object.keys() or Object.entries() would produce an empty array. If you need to loop through a struct's fields programmatically, use its iterator. It returns key-value pairs in a fashion similar to that of Map.

const ResponseType = enum { normal, partial, bad };
const Response = struct {
    type: ResponseType,
    size: usize,
    code: u32 = 200,
    bytes: [8]u8,
};

pub fn getResponse() Response {
    return .{
        .type = .normal,
        .size = 512,
        .bytes = .{ 1, 2, 3, 4, 5, 6, 7, 8 },
    };
}
import { getResponse } from './struct-example-3.zig';

const response = getResponse();
for (const [ key, value ] of response) {
    console.log(`${key} = ${value}`);
}
type = normal
size = 512
code = 200
bytes = [object [8]u8]

The behavior is different when a next function is present. The struct itself would be treated as an iterator in that case.

Extern struct

An extern struct differs from a regular struct in that it has a well-defined memory layout. Individual fields are placed where programs written in the C language expect them to be. Consider the following code:

pub const ExternStruct = extern struct {
    small_int: i16,
    big_int: i64,
};

pub const RegularStruct = struct {
    small_int: i16,
    big_int: i64,
};

pub const ext_struct: ExternStruct = .{ .small_int = 123, .big_int = 4567890123 };
pub const reg_struct: RegularStruct = .{ .small_int = 123, .big_int = 4567890123 };
import { ext_struct, reg_struct } from './extern-struct-example-1.zig';

console.log('Extern:');
console.log(ext_struct.dataView.getInt16(0, true));
console.log(ext_struct.dataView.getBigInt64(8, true));
console.log('Regular (wrong):');
console.log(reg_struct.dataView.getInt16(0, true));
console.log(reg_struct.dataView.getBigInt64(8, true));
Extern:
123
4567890123n
Regular (wrong):
30923
123n

The Zig compiler places small_int of ExternStruct at offset 0 because that's how a C compiler would do it. big_int get placed at offset 8 because i64 has an alignment of 8 and 8 is the nearest position.

For RegularStruct, the Zig compiler follows its own approach, which is to place fields requiring larger alignment further up front. big_int therefore ends up at offset 0 while small_int gets offset 8.

Extern struct is generally what you need when dealing with binary data stored in a file.

Packed struct

A packed struct is commonly used when a struct contains mostly boolean variables:

pub const PackedStruct = packed struct {
    state_a: bool = false,
    state_b: bool = true,
    state_c: bool = false,
    state_d: bool = false,
    state_e: bool = false,
    state_f: bool = false,
    state_g: bool = false,
};

pub const RegularStruct = struct {
    state_a: bool = false,
    state_b: bool = true,
    state_c: bool = false,
    state_d: bool = false,
    state_e: bool = false,
    state_f: bool = false,
    state_g: bool = false,
};

pub const pac_struct: PackedStruct = .{};
pub const reg_struct: RegularStruct = .{};
import { pac_struct, reg_struct } from './packed-struct-example-1.zig';

console.log(pac_struct.valueOf());
console.log(reg_struct.valueOf());
console.log(pac_struct.dataView.byteLength);
console.log(reg_struct.dataView.byteLength);
{
  state_a: false,
  state_b: true,
  state_c: false,
  state_d: false,
  state_e: false,
  state_f: false,
  state_g: false
}
{
  state_a: false,
  state_b: true,
  state_c: false,
  state_d: false,
  state_e: false,
  state_f: false,
  state_g: false
}
1
7

A bool only really needs a single bit in order to represent true/false. A full byte is typically used however. This is why reg_struct.dataView.byteLength gives us 7. Meanwhile, pac_struct is able to keep the same information in just a single byte.

Zigar is designed to handle packed structs holding booleans and very small integers. It can do so in an efficient manner. It can handle pathological cases too:

pub const WeirdStruct = packed struct {
    state: bool = false,
    number: u128 = 123456789000000,
};

pub const weird_struct: WeirdStruct = .{};
import { weird_struct } from './packed-struct-example-2.zig';

console.log(weird_struct.valueOf());
{ state: false, number: 123456789000000n }

The code above places a 128-bit integer a single bit from the beginning of the struct. No one in his right mind would use such an arrangement. But as you can see, Zigar is still giving you the expected result. The scenario that Zigar isn't capable of handling is where you place a complex type like struct or optional into a packed union at a non-byte-aligned position.

Packed struct with backing integer

A packed struct with a backing integer can be casted into a number or bigint:

pub const StructA = packed struct(u32) {
    apple: bool = false,
    banana: bool = false,
    cantaloupe: bool = false,
    durian: bool = false,
    _: u28 = 0,
};

pub const StructB = packed struct(u64) {
    agnieszka: bool = false,
    basia: bool = false,
    celina: bool = false,
    dagmara: bool = false,
    _: u60 = 0,
};
import { StructA, StructB } from './packed-struct-example-3.zig';

const a = new StructA({ apple: true, durian: true });
const b = new StructB({ agnieszka: true, basia: true });
console.log(Number(a));
console.log(BigInt(b));
console.log(a == 9);
console.log(b == 3n);
9
3n
true
true

The constructor of such struct will also accept a number or bigint as initializer:

import { StructA, StructB } from './packed-struct-example-3.zig';

const a = new StructA(9);
const b = new StructB(3n);
console.log(a.valueOf());
console.log(b.valueOf());
{ apple: true, banana: false, cantaloupe: false, durian: true, _: 0 }
{ agnieszka: true, basia: true, celina: false, dagmara: false, _: 0n }

Anonymous struct

An anonymous struct is a literal struct without a specific type:

pub const anonymous = .{
    .hello = 123,
    .world = 3.14,
    .type = .unknown,
};
import { anonymous } from './anonymous-struct-example-1.zig';

console.log(anonymous.valueOf());
{ hello: 123, world: 3.14, type: 'unknown' }

The fields of an anonymous struct are all comptime fields. These can hold comptime-only types such as enum literal (as seen in example above).


Tuple

Clone this wiki locally