How to explain the behavior of modifying the value referenced by the result of @constCast?

const std = @import("std");

const n: u8 = 1;

pub fn main() void {
    const p: *const u8 = &n;
    const p2: *u8 = @constCast(p);
    p2.* = 2; // not panic but a no-op
    
    std.debug.print(
        \\{} {} {}
        \\
        , .{p2.*, p.*, n} // 1 1 1
    );
}

Should it crash instead?

Technically UB, I think. Seg faults for me (Windows, debug, 15.2), just as I expect.

Zig believes you when you lie to it.

6 Likes

@constCast mainly used for those C language interfaces that are poorly written.

Here is a use case of a C interface that might require using @constCast to interact with. As a release interface, it requires that the pointer to the value to be released is a constant pointer.

@constCast is mainly used to handle such legacy C interfaces where the input parameter is actually mutable. In general situations, using @constCast is mostly incorrect.

1 Like

Yes, I know it is a bad use of @constCast in my demo code. I’m just wondering that what is the instruction is generated for p2.* = 2; on Linux. Why doesn’t this instruction cause crashing?

In debug mode, Zig faithfully carried out this value modification.

In addition, from the compilation results, it can be seen that before calling the print function, there is no modification to the rdi register, which means that this print function does not take any input parameters; that is, the printed 1 1 1 has already been hard-coded into the print function at compile time. This is the reason why the printed result is the old value.

As for the reason it didn’t crash, we can check from the compiled output which section n was linked into.

npc1054657282@localhost:~/projects/zig-tests$ nm test | grep "test\.n"
000000000119acc0 d test.n

It is linked into the .data section instead of .rodata. This is the reason there was no crash. A crash in Windows may mean that Windows is stricter during linking.

5 Likes

fyi std has this exact same issue due to bad use of @constCast in std.Io.Reader.ending
https://codeberg.org/ziglang/zig/issues/30070

Here is a use case of a C interface that might require using @constCast to interact with. As a release interface, it requires that the pointer to the value to be released is a constant pointer.

Mutable → Const is always safe implicit cast so I don’t think this is a correct example. Correct example would be a C api that only accepts mutable char* even though it doesn’t mutate it, or struct that contains char* instead of const char* even though its a constant data.

One good example is struct passwd <pwd.h>

3 Likes

Exactly, this example is more common and more general.

Even in C, this is definitely bad code, but since the C standard never assumes that an object pointed to by a const pointer is immutable, this cannot constitute undefined behavior and ultimately becomes legally bad code. I give this example out of my frustration with many anomalies in the C interface of rocksdb.

How is that? It’s been while since I’ve written C. But I do think even in C char* coerces to const char* so I don’t see the problem here. It would be a problem if the function still modifies the pointer (aka breaks the contract). Freeing the pointer is fine though.

This is an callback interface (function pointer) implemented by the user, which is called when it’s necessary to release resources previously allocated by the user. When I saw this interface with a parameter as a const pointer, I couldn’t believe what its purpose actually was, but after reading the context, I had to accept that the timing of its call is intended for the user to release value, even though this interface requires passing a const pointer.

Editor: I’m not even sure if this counts as bad code in C; maybe some patterns just use such an interface to remind users that the data has never changed since it was allocated, even though it is about to be freed. At least it can’t be considered UB, we just have to use @constCast to make it work.

Freeing a pointer doesn’t actually mutate it. It makes accessing the address illegal afterwards. Unfortunately libc also made the historical mistake of making free take void* instead of const void* (C++ fixed this with delete). Which makes it really awkward to do APIs that allocate memory and then return a constant pointer to it.

Zig allocator interface also seems to have bit of smell relating to this https://codeberg.org/ziglang/zig/src/branch/master/lib/std/mem/Allocator.zig#L447

1 Like

Oh, for me this is a philosophical shock. I have always maintained the practice that ‘ownership heap pointers must be mutable, and when used as constants, you need to create an extra const observer pointer for them,’ and the fact that Zig’s Allocator interface requires a mutable pointer for deallocation has kept me thinking this way.

Perhaps in languages like C++ that can annotate ownership, it makes more sense to use const to indicate whether it is mutable, but for languages that lack ownership annotations, I still find it hard to accept that an ownership pointer can be a const pointer.

Const is typically used when you don’t indeed own the memory behind that pointer. But in manual memory managed language you still have to tell the owner that you no longer need that resource. Of course there are also const pointers that you should never release, but the type system in most languages do not have enough information to encode such information, thus leaving such details to documentation.

I don’t quite understand this argument. In my view, a const pointer means that it will not directly cause a change in the state of the position.

Since I consider releasing ownership pointers to be a state change of the location, I think it should be non-const (at least in C).

We should try to be precise with the language. A “const pointer” is a very different thing than a “pointer to const”, but people especially from C land often use the terms interchangeably so confuse the discussion about what’s mutable and what isn’t.

Natural language is indeed a big headache :joy:. In my native language, ‘const pointer’ means pointer to const, while ‘pointer const’ refers to pointer as const.

1 Like

The reason for this is clear: two lines down, the @memset(bytes, undefined) call absolutely needs the type of bytes to be []u8. Calling this a “code smell” is the kind of thing that has me roll my eyes and say “well, if you say so”, since that memset is a useful part of catching use-after-frees in safe modes, and the @constCast is justified in all cases that aren’t programmer errors: if the Allocator actually allocated the memory, then there’s no problem, since it’s legal to write to.

1 Like

True. If you pass pointer to the free/destroy that wasn’t created by one of the allocator’s create/alloc functions you are already making a mistake. So in this case it’s not that bad, and can’t think of a useful allocator that returned read-only memory.