Zig structs vs Java classes

So I recently learned that every .zig file comes with an automatically generated struct which has the same name as the .zig filename but where the file extension has been removed.

This reminds me of how in Java the public classes have the same name as the enclosing .java filename. In particular, the main function in both Java and Zig are alike in that the main function in Java is in a public class and the main function in Zig is in the enclosing file’s automatically generated struct.

In addition, this also means that any user created struct in Zig is more like a nested class in Java since it is already nested in the enclosing file’s struct.

Furthermore, by default, every member in Java’s public classes as well as in Zig’s enclosing files’ structs is private by default, inaccessible to anything outside of the public class or file struct, and in order to make the members public one needs to add a keyword (public in Java, pub in Zig) to make said member accessible to the rest of the world.

2 Likes

Take care not to confuse what you’re calling “members” here and struct fields though, which can just as well be declared at the top level and are always public:

//! A top-level module.

var inner: u32 = 0;
pub const foo: i32 = 123; // Yes, this needs `pub`...

// But this is a field, and all fields are public.
// Mind the comma.
number: u32,

pub fn init() @This() {
    const x: @This() = .{ .number = inner };
    inner += 1;
    return x;
}
// main.zig

const std = @import("std");
const Mod = @import("Module.zig");

pub fn main() void {
    const m = Mod.init();
    std.debug.print("m.number = {}\n", .{m.number});

    std.debug.print("next is {}\n", .{Mod.init().number});
    std.debug.print("next is {}\n", .{Mod.init().number});
    std.debug.print("next is {}\n", .{Mod.init().number});
    std.debug.print("next is {}\n", .{Mod.init().number});
}
$ zig run main.zig
m.number = 0
next is 1
next is 2
next is 3
next is 4
1 Like

Hey @m_szopinski, welcome :slight_smile:

I read your post and realized that you haven’t actually asked a question. I see that you’re making a series of comparisons between Zig structs and Java classes… is there something particular you’d like explained?

Interesting. So it seems that in Zig structs, the member variables and constants actually correspond to static member variables and constants in Java classes, being associated with the struct or class rather than the object.

Whereas the non-static member variables and constants in Java classes correspond to what would be a private mutable struct field in Zig structs, except that in Zig struct fields are always public and immutable after initialization.

One can probably use pointers in the Zig struct fields to emulate the mutability of Java non-static public member variables in classes, since while the pointers are immutable, the contents of the allocated memory on the heap, where the pointers point to, are mutable. But I don’t think there’s any way to get an analogue of non-static private member variables in Zig structs.

As an aside, the fact that Zig struct fields are always public makes Zig structs similar to C++'s structs (which are basically C++ classes except everything is default public), except that one can use the private keyword in C++'s structs to make the fields private but one cannot do the same in Zig because Zig doesn’t have a private keyword (unlike the case for the pub keyword to make things public).

Partially true. Depending on what you’re willing to do, you can build analogous functionality but it may not be “encouraged” or syntactically direct. You could, for instance, make an aligned buffer that gets casted to a type that’s only known in the parent file in the member functions and not exposed via its import.

I would not recommend this, but there’s always a way to make these things happen. In general, Zig doesn’t support private member variables via the ostensible language.

@m_szopinski, here’s an example…

const std = @import("std");

// not public, type only exists in this file
const Hidden = struct {
    x: usize,
    y: bool,
};

// public, type can be seen via @import
pub const Exposed = struct {
    buffer: [@sizeOf(Hidden)]u8 align(@alignOf(Hidden)) = .{ 0 } ** @sizeOf(Hidden),

    pub fn set_x(self: *Exposed, x: usize) void {
        const ptr: *Hidden = @ptrCast(&self.buffer);
        ptr.x = x;
    }
    pub fn set_y(self: *Exposed, y: bool) void {
        const ptr: *Hidden = @ptrCast(&self.buffer);
        ptr.y = y;
    }
    pub fn get_x(self: *const Exposed) usize {
        const ptr: *const Hidden = @ptrCast(&self.buffer);
        return ptr.x;
    }
    pub fn get_y(self: *const Exposed) bool {
        const ptr: *const Hidden = @ptrCast(&self.buffer);
        return ptr.y;
    }
};

We could argue that that you can still see the bytes in the struct, but that’s the case either way because you can read objects as a stream of bytes:

pub fn main() !void {

    var obj: Exposed = .{};

    obj.set_x(30);
    obj.set_y(true);

    std.debug.print("buffer: {any}\n", .{ obj.buffer[0..] });
    std.debug.print("bytes:  {any}\n", .{ std.mem.asBytes(&obj) });
}

Same thing:

buffer: { 30, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }
bytes:  { 30, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }

Again, I’m not recommending this as a design approach. I’m just saying that you can make an “analogy” if that’s the goal.

3 Likes

in Zig struct fields are always public and immutable after initialization

No, structs fields are mutable from a language perspective, there may be other reasons why a field can’t be mutated, for example the struct itself may be in a const, and the memory of that const may reside in the read only section of the executable, but these things are orthogonal reasons that may make it more difficult or impossible, to mutate the field of a struct.


Parameters however are immutable, Pass by value Parameters:

Structs, unions, and arrays can sometimes be more efficiently passed as a reference, since a copy could be arbitrarily expensive depending on the size. When these types are passed as parameters, Zig may choose to copy and pass by value, or pass by reference, whichever way Zig decides will be faster. This is made possible, in part, by the fact that parameters are immutable.

3 posts were split to a new topic: Function Parameter Immutability