Avoid "dependency loop detected" with function pointers

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.

    const std = @import("std");
    const fooCB = *const fn (foo: Foo, val: u8) void;

    const Foo = struct {
        foo_cb: fooCB,
        pub fn init(cb: fooCB) Foo {
            return Foo{
                .foo_cb = cb,
            };
        }
        pub fn callFoo(foo: Foo, val: u8) void {
            foo.foo_cb(val);
        }
    };

    fn bar(foo: *Foo, val: u8) void {
        _ = foo;
        std.debug.print("val {d}", .{val});
    }

    pub fn main() !void {
         var foo = Foo.init(bar);
         foo.callFoo(8);
    }

When trying to compile I get the following error:

    src/main.zig:3:1: error: dependency loop detected
    const fooCB = *const fn (foo: Foo, val: u8) void;
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

How can I solve this in zig?

Welcome to Ziggit!

Shouldn’t this be *const fn(foo: *Foo, val: u8) void or *const fn(foo: *const Foo, val: u8) void?

1 Like

this works:

const std = @import("std");
const fooCB = *const fn (val: u8) void;

const Foo = struct {

    foo_cb: fooCB,

    pub fn init(cb: fooCB) Foo {
        return Foo {
            .foo_cb = cb,
        };
    }

    pub fn callFoo(foo: *Foo, val: u8) void {
        foo.foo_cb(val);
    }
};

fn bar(val: u8) void {
    std.debug.print("val {d}", .{val});
}

pub fn main() !void {
     var foo = Foo.init(&bar);
     foo.callFoo(8);
}

but I’m not sure if this is what you wanted.

ADDITION:

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.

2 Likes

Unfortunately it is Bug #12325

Example code, from the issue, that fails to compile:

pub const DeviceCallback = *const fn (*Device) void;
pub const Device = struct {
    callback: DeviceCallback,
};

EDIT:
In the issue there is a *anyopaque workaround.

pub const DeviceCallback = *const fn (*anyopaque) void;

fn someCallback(p: *anyopaque) void {
    const device: *Device = @ptrCast(@alignCast(p));
}
2 Likes

Thank you for all your help, if it is a compiler bug, I can use the workaround work for now, using * anyopaque. Have gotten it to work

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).

that was a kind of ramblings :slight_smile:

Another possible workaround would be to insert the function pointer directly instead of storing it in a separate declaration:

        foo_cb: *const fn (foo: Foo, val: u8) void,
2 Likes

Another working example:

const std = @import("std");

const Foo = struct {

    data: u8,
    cb: FooCallBack,

    const FooCallBack = struct {
        const FnPtr = *const fn (*Foo, u8) void;
        fptr: FnPtr,
        data: u8,
    };

    pub fn call(foo: *Foo, data: u8) void {
        foo.cb.fptr(foo, data);
    }
};

fn bar(foo: *Foo, data: u8) void {
    std.debug.print (
        "data-1 = {}, data-2 = {}, data-3 = {}\n",
        .{foo.data, foo.cb.data, data}
    );
}

pub fn main() !void {
    var foo: Foo = .{
        .data = 7,
        .cb = .{.fptr = &bar, .data = 8},
    };
    foo.call(9);
}

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.

1 Like

Here is my attempt at answering this question

I don’t think this is a bug in the compiler. I think maybe this is a logic problem in your code.

TLDR;

You are creating something by referencing itself. You need to create it. Then reference it.

Long

I checked your code over and I was wondering how the compiler is supposed to :

  1. Create run time var foo. (the Variable in your code)
  2. With a Stuct called Foo.
  3. The struct has an element foo_cb.
  4. Which has a pointer datatype (fooCB)
  5. This pointer is to reference an “outside of struct” fn called bar
  6. The fn bar requires a pointer to the struct Foo
  7. What is the value of this pointer? If it has not been created yet?

error: dependency loop detected

For what it’s worth here is your code rewritten. It works on zig version 0.13.0-dev.211+6a65561e3

All the best

code

const std = @import("std");
//  const fooCB = *const fn (foo: Foo, val: u8) void;
    const fooCB =fn (foo: Foo, val: u8) void;

    const Foo = struct {
        foo_cb: *const fooCB,

        // added to prove that bar can access the struct itself
        magic_number:u8=42,

        pub fn init(cb: fooCB) Foo {
            return Foo{
              //.foo_cb = cb,
                .foo_cb = &cb,
            };
        }
        pub fn callFoo(foo: Foo, val: u8) void {

            //foo.foo_cb(val);
            foo.foo_cb(foo,val);
        }

    };

    //fn bar(foo: *Foo, val: u8) void {
    fn bar(foo: Foo, val: u8) void {

        // _ = foo; // Removed as I wanted to see if I can acces foo
        std.debug.print("val {d}\n", .{foo.magic_number});
        std.debug.print("val {d}\n", .{val});
    }

    pub fn main() !void {

         //var foo = Foo.init(Foo.bar); // does not work as we reference itself

         var foo:Foo=undefined; // create the instance of Foo so we can reference it
         foo = Foo.init(bar);   // now we can reference it

         foo.callFoo(8);
    }


output

val 42
val 8
1 Like

Constructor

    pub fn init(cb: fooCB) Foo {
        return Foo{
            .foo_cb = &cb,
        };
    }

is confising me a bit.

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.

I do not think that’s the point.
I rewrote your example in a way suggested by @IntegratedQuantum and it’s fine:

const std = @import("std");

const Foo = struct {

    foo_cb: *const fn (foo: *Foo, val: u8) void,
    magic_number: u8 = 42,

    pub fn init(cb: *const fn (foo: *Foo, val: u8) void) 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("val {d}\n", .{foo.magic_number});
    std.debug.print("val {d}\n", .{val});
}

pub fn main() !void {
    var foo = Foo.init(&bar);
    foo.callFoo(8);
}

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.

First; nice code. :slight_smile:

Second; You are right I am wrong.

Third; I got it working. To prove the point.

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.

const std = @import("std");

const fooCB1 = *const fn (foo: *Foo, val: u8) void;
const fooCB2 = *const fn (foo: *Foo, val: u8) void;

const Foo = struct {

  //foo_cb: *const fn (foo: *Foo, val: u8) void,
    foo_cb:fooCB1,
    magic_number: u8 = 42,

  //pub fn init(cb: *const fn (foo: *Foo, val: u8) void) Foo {
    pub fn init(cb: fooCB2) 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("val {d}\n", .{foo.magic_number});
    std.debug.print("val {d}\n", .{val});
}

pub fn main() !void {
    var foo = Foo.init(&bar);
    foo.callFoo(8);
}

2 Likes

:face_with_spiral_eyes:
holy shit!
it’s cool how you managed to “fool” the compiler.

BTW

const fooCB1 = *const fn (foo: *Foo, val: u8) void;
const fooCB2 = fooCB1;

does not work, got all the same
dependency loop detected

LOL

I did exactly the same thing. But I took it out as I did not want to complicate matters.

So for now we have 4 workarounds:

  • using *anyopaque
  • no init, initialize directly
  • do not declare that type, use *const fn (foo: *Foo, val: u8) void everywhere
  • use two identical types for function pointer

@johanadamnilsson, which one do you like more?

What is really puzzles me is that here I have

const reactFnPtr = *const fn(me: *StageMachine, src: ?*StageMachine, data: ?*anyopaque) void;
const enterFnPtr = *const fn(me: *StageMachine) void;
const leaveFnPtr = enterFnPtr;

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.

I am just going to leave this here.

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

}

how about this:

const StateFn = *const fn () ?StateFn;

there is no function workaround this time (Issue #18664)

@mlugg says what is happening:

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.
2 Likes