Void* (C) and *anyopaque (Zig)

It is common practice to use generic pointers in C, typically we write something like this:

int rx_idle_M1(struct edsm *src, struct edsm *me, void *dptr)
{
        struct rxtx_info *rxi = me->data;
        struct io_context *ctx = dptr;
        ....

One does not even have to use explicit casting, it happens automatically (after all, compiler knows how exactly I want to interpret memory, from variable definition).

On the other hand, in Zig I have to write virtually same stuff like this:

    fn idleM1(me: *StageMachine, src: ?*StageMachine, data: ?*anyopaque) void {
        var pd = @ptrCast(*RxData, @alignCast(@alignOf(*RxData), me.data));
//        var pd: *RxData = me.data.?; // oops :(
        var ctx = @ptrCast(*Context, @alignCast(@alignOf(*Context), data));

Obviously, Zig code is more wordy than C code when it comes to casting generic pointers.

Question 1. Is there any way to write it shorter?

Question 2. If I write

var pd: *RxData = me.data.?;

I get pointer type child 'anyopaque' cannot cast into pointer type child 'RxData' error.
But why? I indicated explicitly what kind of pointer I want. Why that @ptrCast magic does not happen automatically in Zig?

I have a guess at the reason. The behavior of void* is C is thought by some to be dangerous because it prevents pointer type checking. In your example it does seem reasonable that coercion should occur automatically to the type of the left-hand side. However, if that were true here then it would also be true for param passing, since assignment and param passing use the same coercion rules. So you’d be able to pass the opaque pointer to any pointer param of any function without type checking, as you can in C, which does seem pretty dangerous.

I can easily do this in Zig too:

// me.msgTo(rx, M1_WORK, &pd.ctx); // pointer pointed to the right place
me.msgTo(rx, M1_WORK, pd); // now pointer intentionally points to some wrong place

ta da!

thread 336606 panic: reached unreachable code
/opt/zig/lib/std/os.zig:3765:18: 0x2297eb in epoll_ctl (server)
        .BADF => unreachable, 

I broke the program by passing valid pointer, which points to wrong place. Compiler can not control this, it is programer concern.

I can’t see why the wrong pointer is allowed there. What is the type of the 3rd param of msgTo, and of pd and pd.ctx?

pub fn msgTo(self: *Self, dst: ?*Self, sqn: u4, data: ?*anyopaque)

pd is also ?*anyopaque, pd.ctx is Context (struct)

Because compiler can not control this. But it is not a problem at all, I was wondering why it can not do generic (opaque) pointer casting as gcc does.

When I write

var ctx = @ptrCast(*Context, @alignCast(@alignOf(*Context), data));

I am kinda telling the compiler - “Trust me, I know what I am doing”.
But… writing

var ctx: *Context = data;

is absolutely the same thing, isn’t it?

That’s an example where the destination type is ?*anyopaque so Zig has to allow any pointer type as the source – there is no way to check the type. That doesn’t necessarily imply that no checking should be done in other situations, namely when the destination type is not ?*anyopaque.

Edit: I was replying here to your msgTo example.

In the case of an explicit assignment I agree that it would be nice to allow it without the casting, unless this cause other problems I’m not aware of.

But if that is allowed, then we have to also allow passing an ?*anyopaque to a param of any pointer type – *usize, *X, etc. This is because coercion works the same way for assignments and param passing. The coercion for param passing when the dest type is not ?*anyopaque is what seems dangerous to me.

Who knows, maybe coercion rules for assignment and param passing could be different in the future in this one special case. I’m just trying to explain why I think it works the way it does.

Ok-ok, I see what you are meaning:

fn func(ptr: *SomeStruct) void {}
// and then
var p: *anyopaque;
func(p);

This is what you consider dangerous, right?

yes, exactly

Compiler does not allow this (at least directly):

const std = @import("std");
const print = std.debug.print;

const Object1 = struct {
    f1: u8 = 1,
    f2: u8 = 2,
    f3: u8 = 3,
};

fn func(o: *Object1) void {
    print("{}\n", .{o});
}

pub fn main() void {

    var o1 = Object1{};
    var p: *anyopaque = &o1;
    func(p);
}

Compiler says:

$ /opt/zig/zig build-exe c.zig 
c.zig:19:10: error: expected type '*c.Object1', found '*anyopaque'
    func(p);
         ^
c.zig:19:10: note: pointer type child 'anyopaque' cannot cast into pointer type child 'c.Object1'
c.zig:5:17: note: struct declared here
const Object1 = struct {
                ^~~~~~

Right, which is why I think it also doesn’t allow this assignment:

var o2: *Object1 = p;

In both cases, the source is an *anyopaque and the destination is not *anyopaque.

Anyway, we can fool compiler^w ourselves around:

const std = @import("std");
const print = std.debug.print;

const Object1 = struct {
    f1: u8 = 1,
    f2: u8 = 2,
    f3: u8 = 3,
};

const Object2 = struct {
    f1: f32 = 1.23,
    f2: f32 = 3.45,
};

fn func(o: *Object1) void {
    print("{}\n", .{o});
}

pub fn main() void {

    var o1 = Object1{};
    var o2 = Object2{};
    var p: *anyopaque = undefined;

    p = &o1;
    func(@ptrCast(*Object1, @alignCast(8,p)));
    p = &o2;
    func(@ptrCast(*Object1, @alignCast(8,p)));
}

Works “fine”:

$ ./cc
cc.Object1{ .f1 = 1, .f2 = 2, .f3 = 3 }
cc.Object1{ .f1 = 164, .f2 = 112, .f3 = 157 }

Yes, you can cast an*anyopaque pointer to any other pointer type, because the compiler has no way to check whether this is correct. But you have to do the cast, the conversion is not automatic. This adds some safety because the cast is explicit (you have to stop and think about it), but is also sometimes inconvenient as you pointed out.

However, you don’t have to cast from some other pointer to *anyopaque. In that case the conversion is automatic. This is an implicit coercion.

1 Like

I completely agree that being explicit in many cases is much better than behind-the-scene games. I just wanted to say that @ptrCast is a little bit “noisy”. It’s not that it is really-really annoying, but

var x: *SomeStruct = some_opaque_ptr;

would be better, and it is nearly explicit cast as I see it.

Anyhow, thanx a lot for your explanations!

1 Like

You could do this:

const std = @import("std");

const StageMachine = struct {
    foo: i8,
};
const Context = struct {
    bar: i8,
};

fn idleM1(me: *StageMachine, data: ?*anyopaque) void {
    var ctx = @ptrCast(*Context, @alignCast(@alignOf(*Context), data));
    me.foo = ctx.bar;
}

// what if we do this instead?
// -> transfer the noisy looking stuff into a function using comptime
fn cptr(comptime T: type, data: ?*anyopaque) T {
    return @ptrCast(T, @alignCast(@alignOf(T), data));
}
fn idleM2(me: *StageMachine, data: ?*anyopaque) void {
    var ctx = cptr(*Context, data);
    me.foo = ctx.bar;
}

pub fn main() void {
    var stage = StageMachine{ .foo = 20 };
    var context = Context{ .bar = 57 };

    std.debug.print("before: {any}\n", .{stage.foo});
    // idleM1(&stage, &context);
    idleM2(&stage, &context);
    std.debug.print("after: {any}\n", .{stage.foo});
}

(I am assuming here that some other code handles nullpointer checking, or there aren’t nulls…)

Personally I like this because it is close to what you want, but it also mentions cptr which gives some context to the programmer “we do some kind of transformation on that type (first argument) and data (second argument) by calling function cptr”.

Instead of having one implicit conversion.

Additionally being explicit here is also good, because now we can have several different functions similar to cptr that do different things (for example check for null pointer and panic in debug builds, or similar things); instead of just having one predefined conversion, that you are stuck with, if it doesn’t match your particular use case.
I think with conversions it is quite likely that different users have different cases, where other things are convenient. IMHO it is best to not choose, if there are multiple “correct” choices and instead let the programmer specify explicitly.
And then have ways to make things less tedious with comptime / meta programming or other techniques.


Are there cases where you always want one thing and have the language exactly do that?
Yes.
But I think in those cases you might also have a different question of:
Do I want to program in some kind of DSL / custom language here? Would it be worth the effort?
Does the language have some other kind of extensibility / meta-programming / macros / etc. so that I can customize it towards my goal without creating a full new language?
What tradeoffs do I really care about and what are specific goals?
What are you gaining and losing by that approach?

2 Likes

Awesome answer! I also thought about some comptime helper/wrapper, but have not tried it yet. In C I would just make some macro.

1 Like

Fine, it works.

pub fn fromOpaq(comptime T: type, ptr: ?*anyopaque) *T {
    return @ptrCast(*T, @alignCast(@alignOf(*T), ptr));
}

Maybe the name is not perfect…