Is there a way to enforce read only behavior for a struct field?
The use case is memory mapped registers in a microcontroller.
Is there a way to enforce read only behavior for a struct field?
The use case is memory mapped registers in a microcontroller.
There is not. The proposal for private fields (which is related but not the same) was closed with this explanation.
The answer to that is probably similar to what youâll hear for read-only fields. Basically, the core team believes that documentation, field names, and (in the future) tooling is a sufficient solution to field access ârestrictions.â
No, not directly. If the struct itself is mutable its fields are too.
You can however use a const pointer to the struct or define getters to avoid accidental mutation.
For memory mapped registers I think a common approach is to hide the address altogether and define a read
method for safe access. Take a look at how microzig does it:
EDIT: struct fields are indeed not mutable if the struct is const, see reply
no a constant value does not let you mutate fields.
const
is not shallow, except for pointers which have their own constness
Yeah youâre right about that of course. I just assumed the struct has to be a var because of other fields/its parent container then you could use a const pointer to it or a getter function.
I think the core of the issue here is that both const
and var
propagate recursively through all fields of a type. There is no way to make one field immutable and another one mutable. Itâs all or nothing.
The only way to effectively encode const
ness into a type are const
pointers, otherwise itâs always up to the user whether to instantiate the type as var
or const
. And even then, the user can still just mutate the pointer itself.
Or you can make the correct usage âobviousâ by e.g. providing a read()
method, which is what is intended by the Zig core team AFAIK.
Thanks for the responses and proposed solutions.
With respect to the private field discussion this seems like a separate issue in my opinion. I am not interested in hiding a field. I would just like a compile time check that no write/assignments are performed to that field.
It would be nice to be able to use const for a field. That to me seems like a consistent use of the keyword (I know that is not for me to say but still).
But I defer to our benevolent overlords on the issue.
FWIW would like to see a filesystem-like access rules system thatâs very different from the typical public/private/protected thing in OOP languages, but this would probably be a whole new âdata centricâ programming language.
The idea is that your whole application state would be organized in a single nested globally visible struct (similar to how code is organized in a single big module tree with a root module at the top).
âŚthen at some central place you define access rules (which parts of the code have read-only, read-write - and maybe no-access - to specific locations in the data tree).
tbh I donât see the point of having anything not public in a programming language.
Restricting data visibility isnât as important as readonly access, but âeverything visible to everybodyâ will massively increase refactoring friction in large code bases (because when something is accessible, it will be used, no matter what the documentation says). E.g. something like Zigâs pub
to restrict inter-module visibility is the bare minimum IMHO.
But if all data is organized in a single filesystem-like struct, something similar to pub
may be needed to shield off specific parts of the data from specific parts of the code.
One might even argue that data visibility is more important than code visibility (e.g. all functions are public but data access is restricted), e.g. if a caller tries to call a public helper function with data the caller may not access, compilation will fail, while another part of the code may call the same function with the same data just fine.
But yeah, this is quite esoteric, not very Zig related and at best a âbrainstorming topicâ
To âenforceâ read only access to fields, you can wrap the struct in an opaque type, and provide methods to access the data. Here is a basic example:
const std = @import("std");
pub const User = opaque {
const Impl = struct {
name: []const u8,
key: u64,
allocator: std.mem.Allocator,
};
pub fn init(allocator: std.mem.Allocator, name: []const u8, key: u64) !*User {
const res = try allocator.create(Impl);
res.* = Impl{
.name = name,
.key = key,
.allocator = allocator,
};
return @ptrCast(res);
}
fn impl(self: *User) *Impl {
return @as(*Impl, @alignCast(@ptrCast(self)));
}
pub fn deinit(self: *User) void {
self.impl().allocator.destroy(self.impl());
}
pub fn getName(self: *User) []const u8 {
return self.impl().name;
}
pub fn keyMatches(self: *User, key: u64) bool {
return self.impl().key == key;
}
pub fn incrementKey(self: *User) void {
self.impl().key += 1;
}
};
pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}).init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const user: *User = try .init(allocator, "alice", 69);
defer user.deinit();
std.debug.print("Hello {s}\n", .{user.getName()});
std.debug.print("Key matches 420: {}\n", .{user.keyMatches(420)});
std.debug.print("Key matches 69: {}\n", .{user.keyMatches(69)});
std.debug.print("Incrementing key\n", .{});
user.incrementKey();
std.debug.print("Key matches 69: {}\n", .{user.keyMatches(69)});
std.debug.print("Key matches 70: {}\n", .{user.keyMatches(70)});
}
Would a comptime field work in your use case? You can assign to a comptime field but its has to be known at comptime and match the default value.
If we look one level below Zig, there is a way to make entire pages as readonly. So if all of the registers are readonly you could just mprotect/VirtualProtect to make the entire page readonly.
I would like to avoid writing boiler plate access functions. This would be cumbersome given the number of read only registers in the memory map of the device I am working with.
Not sure exactly what you have in mind. Can you give a basic example?
Unfortunately, read only fields on my target are quite often 1 bit in size with the next or previous bit having read/write or write only access.
Comptime fields are struct fields that arenât actually there:
const std = @import("std");
const S = struct {
comptime a: i32 = 123,
comptime b: i32 = 456,
};
pub fn main() void {
var s: S = .{};
std.debug.print("{} takes up {d} bytes\n", .{ s, @sizeOf(S) });
s.a = 123;
s.b = 456;
}
test.S{ .a = 123, .b = 456 } takes up 0 bytes
The compiler allows you to assign to these non-existing fields as long as the values match their defaults.
If the comptime field takes up 0 bytes would that not then cause the fields after to become âmisalignedâ with the register bits they should be pointing to when the struct represents a register?
For MMIO youâd use an extern struct, in which case comptime field usage would be a compile error
Well, comptime fields arenât allowed in packed structs anyway. Donât think theyâre of use in your case.
Great, thanks