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.
// 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
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.
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.
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.
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.
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?