-
Notifications
You must be signed in to change notification settings - Fork 3
Struct
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: "[email protected]",
});
console.log(user.id);
console.log(user.name.string);
console.log(user.email);
console.log(user.age);
console.log(user.popularity);
1234n
Bigus Dickus
[email protected]
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: "[email protected]",
age: 32,
address: null,
});
user.address = {
street: '1 Colosseum Sq.',
city: 'Rome',
state: 'NY',
zipCode: '10001',
};
user.print();
Name: Bigus Dickus
E-mail: [email protected]
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: "[email protected]",
age: 32,
address: {
street: '1 Colosseum Sq.',
city: 'Rome',
state: 'NY',
zipCode: '10001',
},
});
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.
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.
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.
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 }
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).