Why can't extern structs have zero sized auto structs as fields?

If you want to a zero sized struct field in an extern struct, it must also be extern. Is this a bug or a feature? :wink:

// /tmp/tmp.zid
test {
    _ = extern struct {
        m: struct {},
    }{};
}

zig 0.16:

$ zig test /tmp/tmp.zig
/tmp/tmp.zig:3:12: error: extern structs cannot contain fields of type 'tmp.test_0__struct_42975__struct_42976'
        m: struct {},
           ^~~~~~~~~

I believe this is working as intended.

It is perplexing to regard a size 0 struct as a special case.

1 Like

Auto structs can include hidden data for debug/safety purposes, e.g verifying ptr cast. So you can not be sure a struct with no fields is 0 sized!

That is not currently the case but is planned.

5 Likes

Hidden data? How does that look?

I’m imagining fields which are void in release builds but non-zero sized in debug builds. Or maybe bare union tags in debug builds.

Well it’s the compiler, so it can just add or remove things if it wants to.

While they will probably only exist in safe modes, the language doesn’t require it.

Gotcha. I guess I can see how special casing them is akward for the compiler.

I guess I don’t see how a user defined, zero sized type might not be zero sized though. And if it must always be zero sized, then extern or auto wouldn’t seem to matter. Unless there are some language features making them incompatible.

you simply can’t define a zero sized type with an auto struct, as the size is an implementation detail of the compiler.

And there is no reason “empty” auto structs should be exempt from hidden safety fields, they can be instantiated and used, even if they shouldn’t be.

2 Likes

Ah ha! My use case is solved by using an enum (u0) which is extern compatible :slightly_smiling_face:.

it doesnt matter, but it is curious that you jumped over extern struct{}…

Yes thats what I was using. Nothing wrong with extern struct - both are equal for my use case. Not really an ah-ha moment but I just didn’t think to use an enum :person_shrugging:

The rules about extern structs could be relaxed. Like, what’s the problem with this:

const A = struct{
    x: u8,
    y: u16
};
const B = extern struct{
    w: u8,
    z: A,
};

I’m stating that I want w to come before z, but I don’t care how z itself is laid out.

FYI void can also be used as a field type in extern containers and is the more straightforward/‘idiomatic’ choice for a zero-sized type.

1 Like

Is your use case to put methods on the type?

This cannot be done at the moment, but what’s the use case for this? Is it to improve performance when using @fieldParentPtr?

I realize it’s kinda niche, but when interfacing with object oriented code, like Windows’ COM, structs need to be laid out according to the inherintance rules, and you are expected to extend their structs. After going through some callbacks, you get your struct back. You don’t really care how your own struct is laid out internally, but you have to embed it into an extern struct so that you can work with the system. In code, it looks like this:

const SystemProvidedStruct = extern struct{
    some_field: u8,
    // Your struct goes here
};

const MyStruct = struct{
    data: u8,
};
const EmbedMyStruct = extern struct{
    header: SystemProvidedStruct,
    my_struct: MyStruct, // doesn't work because MyStruct is not extern
};

pub extern "system" registerObject(*SystemProvidedStruct) void;
var my_var: MyEmbedStruct = ...;
registerObject(&my_var.header);
// At a later time, you get this pointer back, and you obtain your struct
// with @fieldParentPtr
const SystemProvidedStruct = extern struct{
    some_field: u8,
    // Your struct goes here
};

const MyStruct = struct{
    data: u8,
    pub const init: MyStruct = ...;
};
const EmbedMyStruct = extern struct{
    header: SystemProvidedStruct,
    my_struct_bytes: [@sizeOf(MyStruct)]u8 align(@alignerOf(MyStruct)),
};

pub extern "system" registerObject(*SystemProvidedStruct) void;
var my_var: MyEmbedStruct = .{
    .header = ...,
    .my_struct_bytes = std.mem.toBytes(MyStruct.init),
}; 
registerObject(&my_var.header);
// At a later time, you get this pointer back, and you obtain your struct
// with @fieldParentPtr
const my_var: *MyEmbedStruct = @fieldParentPtr("header ", header);
const my_struct: *MyStruct = std.mem.bytesAsValue(MyStruct, &my_var.my_struct_bytes);

In this use case, our data is actually just some type-erased bytes.
What may be worth discussing is that toBytes has the potential for extra copying, which could become a problem after PRO and RLS are removed.

1 Like

That’s an interesting solution. my_struct_bytes would also have to be aligned by MyStruct alignment. Next time I’m doing COM, I’ll check this out.

1 Like

Good catch