How To Pass Anonymous Struct Functions?

I am using the master builds right now, is it no longer possible to use functions in anon structs for work around passing in scope to a deferred call?

pub fn php_raylib_vector3_get_properties(object: [*c]php.zend_object) callconv(.C) *php.HashTable {
    const obj: *php_raylib_vector3_object = php_raylib_vector3_fetch_object(object);
    const props: *php.HashTable = php.zend_std_get_properties(object);
    // const hnd: ?*raylib_vector3_prop_handler = null;

    const body = struct {
        const Self = @This();
        obj: *php_raylib_vector3_object,
        props: *php.HashTable,
        // hnd: ?*raylib_vector3_prop_handler,

        pub fn iter(self: *Self, key: ?*php.zend_string, _: ?*anyopaque) void {
            var ret: ?*php.zval = null;
            var val_zval: php.zval = undefined;

            ret = php_raylib_vector3_property_reader(self.obj, self.hnd, &val_zval);
            if (ret == null) {
                ret = &php.EG.uninitialized_zval;
            }
            php.zend_hash_update(self.props, key, ret);
        }
    }{
        .obj = obj,
        .props = props,
        // .hnd = null,
    };

    zend.ZEND_HASH_FOREACH_STR_KEY_PTR(obj.prop_handler, body.iter);
    _ = zend.ZEND_HASH_FOREACH_END();

    return props;
}
vector3.zig:192:9: error: expected type '*vector3.php_raylib_vector3_get_properties__struct_3227', found '*const vector3.php_raylib_vector3_get_properties__struct_3227'
    body.iter(null, null);
    ~~~~^~~~~
vector3.zig:192:9: note: cast discards const qualifier
vector3.zig:176:27: note: parameter type declared here
        pub fn iter(self: *Self, key: ?*php.zend_string, _: ?*anyopaque) void {
                          ^~~~~

I see this proposal was accepted, but not yet implemented?

At a first glance, your problem might be, that you declare ‘body’ as const.
But the iter method expects a non-const pointer, as the error message states.

2 Likes

I don’t believe that proposal would have any effect on the code you posted here.

1 Like

Seems like as long as there is no state in the struct its fine… This compiles.

const std = @import("std");

pub fn foo(w: f32, body: fn (p: f32) void) void {
    body(w);
}

pub fn main() void {
    foo(@floatCast(32.0), struct {
        fn call(p: f32) void {
            std.debug.print("{d}", .{p});
        }
    }.call);
}

As soon as I add state

const std = @import("std");

pub fn foo(w: f32, body: fn (p: f32) void) void {
    body(w);
}

pub fn main() void {
    foo(@floatCast(32.0), (struct {
        name: *const [5:0]u8,
        fn call(p: f32) void {
            std.debug.print("{d}", .{p});
        }
    }{ .name = "Hello" }).call);
}

I get the error from my post.
error: no field named 'call' in struct 'main__struct_3329'

body.iter(null, null); is equivalent to iter(body, null, null);
body is declared as an immutable constant: const body = ....
But iter expects a pointer to mutable Self: pub fn iter(self: *Self, ...

A possible fix for that is to declare self: *const Self.

2 Likes

In this case, you need the function call to be public. You’re trying to retrieve it from outside the namespace it’s defined in, and you can only see public declarations outside the defining namespace.

1 Like
const std = @import("std");

pub fn foo(w: f32, body: fn (p: f32) void) void {
    body(w + @as(f32, @floatCast(23)));
}

pub fn main() void {
    const bar = struct {
        const Self = @This();

        name: *const [5:0]u8,

        pub fn length(self: Self, p: f32) void {
            std.debug.print("Hello {s} {d}", .{ self.name, p });
        }
    }{ .name = "Joles" };

    foo(32.0, bar.length);
}
test.zig:18:19: error: no field named 'length' in struct 'test.main__struct_2633'
    foo(32.0, bar.length);
                  ^~~~~~
test.zig:8:17: note: struct declared here
    const bar = struct {
                ^~~~~~

So I cannot pass in a function of an anonymous struct, but I can execute it. So foo(32.0, bar.length); does not work, but I can call bar.length(32.0);. I would have to pass in all of bar into foo i.e foo(32, bar) and then inside foo, I can call body.length(). So this works:

const std = @import("std");

pub fn foo(w: f32, body: anytype) void {
    body.length(w + @as(f32, @floatCast(23)));
}

pub fn main() void {
    const bar = struct {
        name: []const u8,

        pub fn length(self: @This(), p: f32) void {
            std.debug.print("Hello {s} {d}\n", .{ self.name, p });
        }
    }{ .name = "Joles" };

    foo(32.0, bar);
}

This is less than ideal… I can use generics to get a type for it so it’s not just anytype.

const std = @import("std");

pub fn foo(comptime T: type, w: f32, body: T) void {
    body.length(w + @as(f32, @floatCast(23)));
}

pub fn main() void {
    const bar = struct {
        name: []const u8,

        pub fn length(self: @This(), p: f32) void {
            std.debug.print("Hello {s} {d}\n", .{ self.name, p });
        }
    }{ .name = "Joles" };

    foo(@TypeOf(bar), 32.0, bar);
}

You’re confusing declarations and fields.

A declaration is part of the type, not part of the instance. There’s an exception, which is that you can call, but not access, a member function: a declaration where the first argument is of the type of the namespace.

Try running this:

test "declaration things" {
    const allocator = std.testing.allocator;
    const Array8 = std.ArrayList(u8);
    const array_8 = Array8.init(allocator);
    try expectEqual(Array8.init, array_8.init);
}

You’ll see that you can access Array8.init, but not array_8.init.

init is not a member function, but member functions are subject to the same constraint:

test "call vs. access for member functions" {
    const allocator = std.testing.allocator;
    const Array8 = std.ArrayList(u8);
    var array_8 = Array8.init(allocator);
    defer array_8.deinit();
    // This works:
    try array_8.append(4);
    // This doesnt:
    try expectEqual(Array8.append, array_8.append);
}

I suggest giving things names, while you work out the various relationships that they have. I don’t know what prompted you to try writing code like var variable = struct {...}{initializer}.field; but there’s never any reason to do this.

2 Likes

Maybe I am not using the right words, but the intent is pass a function pointer without having intimate knowledge of outer world and this function pointer can carry its own scope. I thought the intent from the code I provided was showing this. However from what you’ve said, and since there is no closures, or lambdas in Zig, I have the following options:

1. Pass in the entire struct and make the caller function aware the callee:

const std = @import("std");

pub fn foo(comptime T: type, w: f32, body: T) void {
    body.length(w + @as(f32, @floatCast(23)));
}

pub fn main() void {
    const bar = struct {
        name: []const u8,

        pub fn length(self: @This(), p: f32) void {
            std.debug.print("Hello {s} {d}\n", .{ self.name, p });
        }
    }{ .name = "Joles" };

    foo(@TypeOf(bar), 32.0, bar);
}

2. Pass in the struct and struct function pointer like in C/C++

const std = @import("std");

pub fn foo(
    comptime ContextType: type,
    w: f32,
    context: *ContextType,
    callback: fn (*ContextType, f32) void,
) void {
    callback(context, w + 23.0);
}

pub fn main() void {
    const bar_def = struct {
        const Self = @This();
        name: []const u8,

        pub fn length(self: *Self, p: f32) void {
            std.debug.print("Hello {s} {d}\n", .{ self.name, p });
        }
    };

    var bar: bar_def = .{ .name = "Joles" };

    foo(bar_def, 32.0, &bar, bar_def.length);
}

So when you say:

It’s the explicit reason of wanting a closure, or lambda. Not requiring the caller function to have intimate knowledge of the callee function, but also allowing the callee function to have context. Here is how I’d write this idea in C.

#include <stdio.h>

// Define your struct with data
typedef struct {
    const char *name;
} Bar;

// The callback function that uses the context to access struct data
void bar_length(void *context, float p) {
    Bar *self = (Bar *)context;
    printf("Hello %s %.1f\n", self->name, p);
}

// Function foo that accepts w, a context pointer, and a callback function
void foo(float w, void *context, void (*callback)(void *, float)) {
    float p = w + 23.0f;
    callback(context, p);
}

int main() {
    // Initialize the struct
    Bar bar = { .name = "Joles" };

    // Call foo with w, context, and callback function
    foo(32.0f, &bar, bar_length);

    return 0;
}

I think the closest I will get in Zig is the second option repeated below:

// Second option
pub fn foo(
    comptime ContextType: type,
    w: f32,
    context: *ContextType,
    callback: fn (*ContextType, f32) void,
) void {
    callback(context, w + 23.0);
}

This allows foo to not require the context(struct) to have an arbitrary member requirement of length.

// First option 
pub fn foo(comptime T: type, w: f32, body: T) void {
    body.length(w + @as(f32, @floatCast(23)));
}

Trying to use the former declaration is invisible to what type is required for the struct’s callee to be named or even the required parameters declared.

You can use anytype to avoid explicitly passing the context type.

pub fn foo(
    w: f32,
    context: anytype,
    callback: fn (@TypeOf(context), f32) void,
) void {
    callback(context, w + 23.0);
}
3 Likes

Ok, that clarifies things. Zig doesn’t have anything like a closure, if you want a behavior + state you have to provide that yourself.

This is one way to do it, using a type-generic function: this will specialize the function foo for each type of context object you call it with.

Another way is to use type erasure, this doesn’t involve generics and so the function doesn’t get specialized.


pub const Context = struct {
    state: *anyopaque,
    callback: *const fn (*anyopaque, f32) f32,

    pub fn call(ctx: Context, w: f32) f32 {
        return ctx.callback(ctx.state, w);
    }
};

pub const ContextImpl = struct {
    length: f32,

    pub fn callback(ctx: *anyopaque, w: f32) f32 {
        const context_impl: *ContextImpl = @alignCast(@ptrCast(ctx));
        return context_impl.length + w;
    }
};

test "type erased context object" {
    var an_impl = ContextImpl{ .length = 45.5 };
    var a_context = Context{
        .state = @ptrCast(&an_impl),
        // Note: ContextImpl, not an_impl:
        .callback = &ContextImpl.callback,
    };
    try std.testing.expectEqual(91, a_context.call(45.5));
}

If you need to create multiple instances of the callback, especially if you need to store them (struct field, queue, etc.), this gives you one concrete type, which works for anything as long as it can fulfill the interface.

3 Likes