Hi, so I am relatively new to zig but i am trying to learn. I have mostly developed in c so it is that lens I mostly trying to solve problems though which seams to give me a lot of problems when dealing with function pointers in newer languages.
So what I want to do is to pass in a function that will be used later, the function needs to pass in a reference to the object itself. I created a small example to replicate the problem.
If you want your bar() to have *Foo as it’s first parameter, just make it a method of Foo. But bar() is stand alone function, and I think it must not have *Foo as an argument.
Nevertheless, it seems to me that something is a bit wrong in a pattern / design decision from original post. We have a callback, which is not a part of Foo, it’s a standalone function, but for some reason it wants a Foo (or a pointer to) as an argument. The reason why it wants it is unclear since this argument is ignored anyway. I think if this callback needs to be outside Foo, probably it’s interface should be designed somehow else - for example, you can pass some parts of Foo to it, but not the entire struct (or a pointer to it).
It seems that without init() constructor everything is ok.
When an instance of Foo is created via init() I get same error.
And it’s strange… here I have 3-level construction more or less similar by intention to OP, but it’s ok, no dependency loop detected.
Here we are passing a function to it, not a function pointer. But what does this mean? What is really passed, still an address of bar? And then we are assigning an address of this parameter to struct field which is also a no-no since arguments are in temporary memory. Yes, I run the code, it works as expected and this puzzles me.
So, it is declaring a type const FooCB = *const fn (foo: *Foo, val: u8) void that “breaks” the compiler in this case (but not always!). And using this explicitely everywhere is ok, it’s just a bit annoying and less readable.
I created two identical fooCB, I called them 1 and 2. If you use just fooCB1 or just fooCB2 it does not work. But if you use fooCB1 and then FooCB2 it works fine.
and everything in the garden’s lovely, no error: dependency loop detected.
So it’s absolutely unclear (for me) what exactly triggers this compile error.
const fooCB = *const fn (foo: *Foo, val: u8) void;
const Foo = struct {
foo_cb:fooCB,
magic_number: u8 = 42,
pub fn init(cb: fooCB) Foo {
return Foo {
.foo_cb = cb,
};
}
pub fn callFoo(foo: *Foo, val: u8) void {
foo.foo_cb(foo, val);
}
};
fn bar(foo: *Foo, val: u8) void {
std.debug.print("\nbefore val {d}\n", .{foo.magic_number});
foo.magic_number=val;
std.debug.print("after val {d}\n", .{foo.magic_number});
}
pub fn main() !void {
//var foo = Foo.init(&bar); // does not work as it reference itself
var foo:Foo=undefined;
foo= Foo.init(&bar);
foo.callFoo(8); // will change the magic number to 8
foo.callFoo(11); // will change the magic number to 11 but not before showing what it was originally
}
The error here happens because we only have lazy resolution (which permits recursive definitions) for structs, unions, enums, and opaques. It’s not exactly a duplicate, but is heavily related to #12325.
and in #12325
Should be fixed by implementing lazy pointer types, lazy array types, or both.
Thanks again so much, super fun to see how much all of you investigate the problem and find workarounds.
So as i said a bit new to zig but this is my reasoning about the solutions.
* anyopaque will be a bit annoying for needing to pointer cast and feels a bit unsafe.
no init may work for now however I may need in the future to do some additional setup so would prefer to be able to do that setup in the init function and not create the object and then call a setup function.
using *const fn (foo: *Foo, val: u8) void everywhere will be annoying however for now it will only be on 2 places so will probably be fine for now else in the future i may switch to use two identical types.