zig
zig copied to clipboard
Proposal: Write-only type qualifier
Currently as part of Zig's type qualifiers const
is offered to indicate read-only memory access through say a pointer. For whatever reason however C, C++ and other derivative languages often lack a (standard) generalization of this to write-only memory access, Zig being one such case.
The proposal is pretty simple, a new qualifier should be added to Zig to specify a write-only restriction on pointers so that something like the following can be written (using wonst
to represent this write-only qualifier for lack of my ability to generate a better name):
var foo: i32 = undefined;
var bar: *wonst i32 = &foo;
var baz: *i32 = &foo;
bar.* = 5;
// bar.* += 5; // Error, implicitly reads through the pointer
bar = baz; // Fine, restricting access similar to *i32 -> *const i32
// baz = bar; // Error, cannot widen access to a read/write pointer
As for why someone might want such a feature there are a number of reasons:
- Other languages such as C#, GLSL and HLSL to name a few have effectively a way to do this via
out
parameters (differing from say GLSL'sinout
for example which is closer to the behavior of a standard pointer andin
which would be a constant pointer). Zig has no way to match such a construct other than by using a standard pointer with both read and write access. - Better self documenting code for out parameters on functions, similar to how
const
-correctness can help imply the intent of what is passed to a function (e.g. a constant pointer vs a standard pointer will indicate if the data will be touched and if the caller should be concerned about that). Write-only qualification helps indicate that the pointed value will only be written to so the caller can be confident in not initializing what they pass in knowing it will never be read from whereas this is unclear when standard pointers are used for this instead. - Better safety around special memory types. When working with MMIO registers or memory mapped across hardware (PCIe for example) often the act of reading from an address itself may have side effects, negative performance implications or may even be undefined behavior which ideally should be better guarded against. By qualifying a pointer as write-only it would prevent any accidental reads (if one wrote
foo.* += 5;
without thinking much about it for example) as well as giving the compiler more restrictions to avoid any "clever" optimizations it may do which may themselves involve potentially bugprone reads hidden to the user on the language level. - Potentially better compiler optimizations. I am not sure of any direct example of this but there are likely some niche cases where the compiler may be able to assume more about something if it knows it is only able to ever be written to rather than having both read/write access as it would right now.
Apologies if this was proposed already but I did not see it anywhere upon searching. The closest I saw was #1664 which has similar desires for low-level memory access. I think this proposal would cover that issue's use case as well via read-only and write-only qualified volatile pointers if I am understanding correctly while also allowing for benefits in typical day to day code (rather than it being something super specialized to something like MMIO only).
IMO, within Zig, return values are almost always preferable to out-parameters, because the compiler can catch forgetting to return a value (you have to explicitly provide undefined
), while it cannot catch forgetting to write to an out parameter (because it might be intentional - the option of this would be more clearly documented to the caller by returning an optional type).
For multiple returns you can use a struct with named members.
That said, all other reasons (modelling other language's APIs, MMIO, and more compiler guarantees) seem sound to me.
For keyword, since the use case won't be too common, I think simple writeonly
is fine.
IMO, within Zig, return values are almost always preferable to out-parameters
While I agree it's often preferable, I don't think we should disregard the use case. There are many instances in non-embedded and non-FFI code where you need out parameters, e.g.
- initializing structs which need to be pinned,
- optional out parameters & error diagnostics (like
std.zig.CrossTarget.ParseOptions.Diagnostics
), - basic runtime memory operations like
std.mem.copy
Each these would benefit in using this modifier.