Is downgraded by @ptrCast pointer safe to read via original pointer?

I have a C function which receives u32 pointer and writes there its answer. But variable where I want to store it is usize. I can use one of two approaches:

  1. create tmp variable and then make an assigment
  2. use @ptrCast like in example below
pub fn main() !void {
    var size: usize = 0;
    std.debug.print("{d}\n", .{size}); //0
    my_func(@ptrCast(&size));
    std.debug.print("{d}\n", .{size}); //10
}

fn my_func(p: [*c]u32) void {
    p.* = 10;
}

But I am not sure that it is safe to read the size variable after I had written to it via downgraded pointer. It works but will it be safe on all architectures with different endians?

Doing so will break on 64 bit big endian systems. So its probably not a good idea. On BE result would be not 10 but 10 << 32 instead.

This is undefined behaviour (in C as well), due to aliasing rules. A compiler is free to assume that the usize and the u32 simply do not reside in the same memory and optimize using that assumption, so it’s not even guaranteed to behave correctly on little endian systems.

In other words, don’t do 2. Create a temporary. Unless you’re on a super hot path, the performance difference is negligible.

Side note: big endian systems are basically non-existent nowadays.

EDIT: wording
EDIT2: I was wrong, see this thread

1 Like

What about following code? It shouldn’t be optimized away for little endian in such case ?(@ptrCast replaced with @ptrFromInt(@intFromPtr()))

It works for both le and be. So it should be safe from optimizations?

Checked for test fails with: zig build test -Dtarget=aarch64_be-linux-musl -fqemu and zig build test -Dtarget=aarch64-linux-musl -fqemu

// Assume that all the types passed here are correct so no checks for shortness
fn downgrade_ptr_cast(ptr: anytype, comptime T: type) *T {
    const ptr_type = @TypeOf(ptr);
    const int_type = std.meta.Child(ptr_type);

    return switch(std.lang.Endian.native) {
        .little => @ptrFromInt(@intFromPtr(ptr)),
        .big => @ptrFromInt(@intFromPtr(ptr)+@sizeOf(int_type)-@sizeOf(T)),
    };
}

test "simple test" {
    var size: u16 = 0;

    try std.testing.expectEqual(size, 0);

    downgrade_ptr_cast(&size, u8).* = 0xff;

    try std.testing.expectEqual(size, 0xff);
}

Hmm, actually, I retract my previous comment, Zig does not actually do strict aliasing (as opposed to C), at least as of this thread from last year. So it might be legal.

That means @AndrewKraevskii’s comment stands, even though big endian systems these days are few and far between.

1 Like

It looks like this would be technically correct… but, I don’t see why you’d want this? It would certainly break when compiled with the C backend.

It is safe in the sense that it is well-defined and does not invoke illegal behavior.

However it is a strange operation to do, and there is not enough context in your post for me to advise.

4 Likes

Tbh I am trying to achieve “better syntax” at a cost of shooting my leg

I have a C api which returns me array of data. Size of it is written to a pointer passed as parameter. But parameter is u32 and not usize so I am just creating new slice and pass there &slice.len which I need to downgrade…

1 Like

This is not the Zig way. The Zig way is to achieve best syntax while not shooting yourself in the leg. If you have not reached this peak, you have more to learn. It is possible to achieve.

If you share more details we can help.

1 Like

What’s wrong with just coercing your u32 into a usize?

const size: usize = blk:{
    var temp: u32 = undefined;
    my_func(&temp);
    break :blk temp;
};
1 Like

Not sure if you aware of the caveats with doing this, but manually changing the len field of a slice is not often something you want to be doing if your slices are heap-allocated. That may not be your case here, but this is what you can expect if so, and you don’t implement the logic to change it back to the actual capacity before freeing.

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    var buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);

    buffer.len = 512;
}
zig build run
thread 387729 panic: Invalid free
1 Like

You don’t describe the shape of the C API, so I’m going to assume something like this:

data_t *c_func(uint32_t *out_len);

I would then probably do something like this in Zig:

const slice = blk: {
    var len: u32 = undefined;
    const ptr = c_func(&len);
    break :blk ptr[0..len];
};
1 Like

Yeah, the shape of C API is correct

Thanks for one more method. When compiled on ReleaseFast it outputs basically the same assembly without using fancy magic in source code… But I think I will stick with my gun powered spaghetti monster because I also need to deconstruct the slice at some point for C API which would otherwise fail on BE…

Getting into language lawyering a bit, but usize isn’t guaranteed to be at least as big as u32.

comptime {
    @compileLog(@sizeOf(usize));
}
$ zig build-exe -fno-emit-bin 16.zig -target x86_16-freestanding
16.zig:2:5: error: found compile log statement
    @compileLog(@sizeOf(usize));
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~

...omitted...

Compile Log Output:
@as(comptime_int, 2)
2 Likes