#Closures & Events

1 messages · Page 1 of 1 (latest)

vague pollen
#

This post is really just to have talks about ways to improve this design so it's cleaner and more efficient. Not really to go a different approach altogether.

Context

So I've been experimenting with Zig for a large project the past week and a half. I've come across the lack of closures and any built-in event system. So I made both.

The event system works pretty good and it looks clean. In my old codebase I didn't have to use any data types that weren't primitives. So I stuck to ints, byte arrays, and enums: https://github.com/Quaint-Studios/Sustenet/tree/production-prep.

I wrote a gist that shows examples on how to use the Event, Closures, and storing functions to be used later.

Gist

Zig invokable Events and Closures. GitHub Gist: instantly share code, notes, and snippets.

#

The Ugly

Right now the approach I have to do closures that aren't comptime is something like what you see below. And I want to vomit with how overengineered it looks and feels. But, it works. Here it is:

const std = @import("std");

const ArrayList = std.ArrayList;
const BaseServer = @This();

onConnection: EventT1(i32),
server_type_name: []const u8,

pub fn new() BaseServer {
    return BaseServer{
        .onConnection = EventT1(i32).init(std.heap.page_allocator),
        .server_type_name = "Cluster Server",
    };
}

pub fn start(self: *BaseServer) !void {
    // ...
    {
        const func = struct {
            fn create() type {
                const Context = struct {
                    server_type_name: ?*[]const u8,
                    pub fn setup(this: *@This(), server_type_name: *[]const u8) void {
                        this.server_type_name = server_type_name;
                    }
                    pub fn call(this: *@This(), id: i32) void {
                        BaseServer.debugServer(this.server_type_name.?.*, "Client#{} has connected.\n", .{id});
                    }
                };

                return struct {
                    var context = Context{ .server_type_name = null };
                    pub fn setup(server_type_name: *[]const u8) void {
                        context.setup(server_type_name);
                    }
                    pub fn call(id: i32) void {
                        return context.call(id);
                    }
                };
            }
        };
        const ctx = func.create();
        const call = ctx.call;
        ctx.setup(&self.server_type_name);
        try self.onConnection.add(call);
        self.onConnection.invoke(1);
    }
    // ...
}

/// A Zig implmenetation of C#'s `Action<T>`.
pub fn EventT1(comptime _T: type) type {
    return struct {
        invokes: List,

        pub const T = _T;
        const List = ArrayList(*const fn (T) void);

        const Self = @This();

        pub inline fn init(allocator: std.mem.Allocator) Self {
            return Self{ .invokes = ArrayList(*const fn (T) void).init(allocator) };
        }

        pub inline fn add(self: *Self, callable: *const fn (T) void) !void {
            try self.invokes.append(callable);
        }

        pub inline fn addSlice(self: *Self, callables: []const *const fn (T) void) !void {
            try self.invokes.appendSlice(callables);
        }

        pub inline fn clear(self: *Self) void {
            self.invokes.clearAndFree();
        }

        pub inline fn invoke(self: *Self, arg: T) void {
            for (self.invokes.items) |callable| {
                callable(arg);
            }
        }

        pub inline fn deinit(self: *Self) void {
            self.invokes.clearAndFree();
            self.invokes.deinit();
        }
    };
}

// Added just to make it easier for people to copy, paste, and test.
pub fn debugServer(server_type_name: []const u8, comptime msg: []const u8, args: anytype) void {
    // Utilities.printMsg("({s}) " ++ msg, .{ server_type_name, args });

    std.debug.print("({s}) " ++ msg, .{ server_type_name, args });
}

test "Closure with context" {
    var server = BaseServer.new();
    try server.start();
    std.debug.print("It works!", .{});
}

I haven't tried it with other types yet. But I got it to use a pointer. You can copy the code above to try it out yourself. I tested it with v0.13.0.

round turtle
vague pollen
#

The Closure by Itself

This is just a simpler block of code for people to test with.

const std = @import("std");
const Self = @This();
value: []const u8,

pub fn start(self: *@This()) !void {
    const func = struct {
        fn create() type {
            const Context = struct {
                value_ptr: ?*[]const u8,
                pub fn setup(this: *@This(), value_ptr: *[]const u8) void {
                    this.value_ptr = value_ptr;
                }
                pub fn call(this: *@This(), id: i32) void {
                    std.debug.print("Test ID {} and value {s}\n", .{ id, this.value_ptr.?.* });
                }
            };

            return struct {
                var context = Context{ .value_ptr = null };
                pub fn setup(value_ptr: *[]const u8) void {
                    context.setup(value_ptr);
                }
                pub fn call(id: i32) void {
                    return context.call(id);
                }
            };
        }
    };
    const ctx = func.create();
    const call = ctx.call;
    ctx.setup(&self.value);
    call(5);
    // try self.onConnection.add(call);
    // self.onConnection.invoke(1);
}

test {
    var closure = Self{ .value = "Test Value" };
    try closure.start();
}
vague pollen
#

I mixed it in quickly before I had to get back to work and produced something that seemed less stupid than my initial implementation.

const std = @import("std");
const ArrayList = std.ArrayList;

test {
    var event = EventT1(i32).init(std.heap.page_allocator);
    defer event.deinit();

    const event1 = struct {
        action: Action(i32, void) = .{ .compute = compute },
        val: @Vector(3, f32),
        fn compute(action: *Action(i32, void), arg: i32) void {
            const self: *@This() = @alignCast(@fieldParentPtr("action", action));
            std.debug.print("Value: {} and {}\n", .{ arg, self.val });
        }
    };
    const event1s = event1{ .val = @Vector(3, f32){ 1.0, 2.0, 3.0 } };
    try event.add(@constCast(&event1s.action));
    event.invoke(64);
}

pub fn Action(comptime T: type, comptime R: type) type {
    return struct {
        compute: *const fn (*Action(T, R), T) R,
    };
}

/// A Zig implmenetation of C#'s `Action<T>`.
pub fn EventT1(comptime _T: type) type {
    return struct {
        invokes: List,

        pub const T = _T;
        const List = ArrayList(*Action(T, void));

        const Self = @This();

        pub inline fn init(allocator: std.mem.Allocator) Self {
            return Self{ .invokes = ArrayList(*Action(T, void)).init(allocator) };
        }

        pub inline fn add(self: *Self, callable: *Action(T, void)) !void {
            try self.invokes.append(callable);
        }

        pub inline fn addSlice(self: *Self, callables: []const *Action(T, void)) !void {
            try self.invokes.appendSlice(callables);
        }

        pub inline fn clear(self: *Self) void {
            self.invokes.clearAndFree();
        }

        pub inline fn invoke(self: *Self, arg: T) void {
            for (self.invokes.items) |callable| {
                callable.compute(callable, arg);
            }
        }

        pub inline fn deinit(self: *Self) void {
            self.invokes.clearAndFree();
            self.invokes.deinit();
        }
    };
}
#

That's using what you did but also mixed with my generic events. @round turtle

#

I'll try to revisit it tonight and clean it up some more.

alpine kindle
#

Just as a word of caution, I would generally suggest not wrapping things in structures unless you actually have a good reason to
It seems to me that most of these types are unnecessary.

#

Like - an arraylist of function pointers seems like what you're actually wanting there, less I'm missing something

#

Why make one line of code into 200? 😄

#

Or, if each function pointer has some additional context that the callbacks want to have access to, then perhaps: an arraylist of fnptr and *anyopaque.