Higher order functions in zig?

So I was trying to catch SIGINT while learning the zap framework, and I needed to interop with C to use signal.h. So I wrote a wrapper function that would call zap to stop if ctrl+c was called

fn zapstop(_: c_int) callconv(.C) void {
    zap.stop();
}

And passed it to signal

const csignal = @cImport({
    @cInclude("signal.h");
})

// ...

pub fn main() !void {
    // ... other stuff ...
    csignal.signal(csignal.SIGINT, zapstop);
    // ... other stuff ...
}

Now I wanted to write a function that takes in a function and returns another function with the correct signature for passing to csignal.signal. Essentially, something simple where *const fn () void maps to *const fn (c_int) callconv(.C) void. I tried doing the following but it didn’t work.

fn signal_wrapper(handler: *const fn () void) *const fn (c_int) callconv(.C) void {
    return fn wrapped(_: c_int) callconv(.C) void {
        handler();
        return;
    }
}

I found this article which describes how to use structs within functions to capture variables. I came up with the following minimal working example, however it doesn’t seem to compile, and the function signature is wrong, it contains an extra self param. Any ideas how to do what I’m trying to do? (transform between function signatures)?


MWE:

const std = @import("std");
const builtin = @import("builtin");
const csignal = @cImport({
    @cInclude("signal.h");
});

const SignalHandler = struct {
    a: std.mem.Allocator,
    handlers: handler_map,

    const handler_map = std.AutoHashMap(c_int, *const fn (c_int) callconv(.C) void);
    const Self = @This();
    pub fn init(a: std.mem.Allocator) Self {
        return Self{
            .a = a,
            .handlers = Self.handler_map.init(a),
        };
    }
    pub fn deinit(self: *Self) void {
        self.handlers.deinit();
    }

    pub fn wrap(self: Self, handler: *const fn () void) *const fn (c_int) callconv(.C) void {
        const Closure = struct {
            hfn: *const fn () void,

            const CSelf = @This();
            pub fn f(cself: *Self, _: c_int) callconv(.C) void {
                cself.hfn();
                return;
            }
        };

        const C = self.a.create(Closure) catch {
            std.process.exit(1);
        };
        C.* = Closure{ .hfn = handler };

        return C.f;
    }

    pub fn register(self: *Self, sig: c_int, handler: *const fn (c_int) callconv(.C) void) !void {
        try self.handlers.put(sig, handler);
        _ = csignal.signal(sig, handler);
    }
};

fn wrapme() void {
    std.log.info("i love being wrapped", .{});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const a = gpa.allocator();
    defer _ = gpa.deinit();

    var sigh = SignalHandler.init(a);
    defer sigh.deinit();

    const mewrapped = sigh.wrap(wrapme);
    try sigh.register(csignal.SIGINT, mewrapped);

    std.debug.print("Hello, {s}! (using Zig version: {})", .{ "world", builtin.zig_version });
}

Compilation error:

cocoa@TheTreehouse /t/tmp.6cpLkjpnMq [1]> zig build-exe -lc closures.zig                                                         (self) 
closures.zig:41:20: error: no field named 'f' in struct 'closures.SignalHandler.wrap.Closure'
        return C.f;
                   ^
closures.zig:26:25: note: struct declared here
        const Closure = struct {
                        ^~~~~~
referenced by:
    main: closures.zig:62:27
    callMain: /home/cocoa/.local/bin/zig-linux-x86_64-0.13.0/lib/std/start.zig:524:32
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

I think the Self there should be a CSelf.

Yess, my bad. However that doesn’t seem to change the error.

cocoa@TheTreehouse /t/tmp.6cpLkjpnMq [1]> zig build-exe -lc closures.zig                                                         (self) 
closures.zig:41:18: error: no field named 'f' in struct 'closures.SignalHandler.wrap.Closure'
        return C.f;
                 ^
closures.zig:26:25: note: struct declared here
        const Closure = struct {
                        ^~~~~~
referenced by:
    main: closures.zig:62:27
    callMain: /home/cocoa/.local/bin/zig-linux-x86_64-0.13.0/lib/std/start.zig:524:32
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

I am not sure, but I don’t think this is possible with Zig directly.

You basically try to create a new function at runtime, sort of similar to javascript’s bind function that allows to turn a method call with receiver into the partially applied function that only expects the remaining arguments.

I don’t think there is a similar construct in Zig.

I vaguely imagine that you could do something similar to jit code generators and generate a function stub at run time (by doing your own runtime code generation) and then embed the closure data within that, but I haven’t really done this sort of thing.

I think the more usual way to handle this is to install one global signal handler and then that handler uses state to manage if there are multiple ways the signal should be handled.

But I am not super certain, maybe others have different approaches.


You can do what the article does, using:

.{
    .ptr = C,
    .runFn = Closure.f
};

where .ptr is the cself argument and .runFn is the Closure, but then you are back to the question where to use/store that and how to get a function that matches the signature required for the signal handler.

So for use with the signal handler I think you would need to use a global handler, or generate code.

Well another option is to actually pass the to be wrapped function as a comptime parameter to the wrap function:

pub fn wrap(_: Self, comptime handler: fn () void) *const fn (c_int) callconv(.C) void {
    return struct {
        pub fn f(_: c_int) callconv(.C) void {
            handler();
            return;
        }
    }.f;
}

But that only works at comptime by passing the function body of a function (function body types are different from function pointer types, the former only work at comptime but can be coerced to function pointers), so you can’t for example use this with a function that is loaded from some dynamic library or depends on run-time control flow.

2 Likes

There is stuff in std for dealing with signals.
It’s pretty much the raw posix api though,
it’s not documented, so you’ll have to read man pages if you need more info

const std = @import("std");

pub fn main() !void {
    const act = std.posix.Sigaction{
        .handler = .{ .handler = handle },
        .mask = std.posix.empty_sigset,
        .flags = 0,
    };
    std.posix.sigaction(std.posix.SIG.INT, &act, null);
    while (true) {
        std.debug.print("HI", .{});
        std.Thread.sleep(1 * std.time.ns_per_s);
    }
}

fn handle(_: i32) callconv(.c) void {
    std.debug.print("hewo", .{});
    std.Thread.sleep(1 * std.time.ns_per_s);
    std.process.exit(0);
}
3 Likes

Zig is not a functional language. So doing HOF in Zig will be painful, imo, I would just do it in a simple way.

If all information is available at compile time then doing higher-order functions is not particularly hard, just that the syntax is a little clunky but I’m still hopig that we might get function declaration expressions at some point!

But if you’re doing closures over (is that the correct term?) runtime data it does get a bit tricky it seems.

What are function declaration expressions?

1 Like

Functions as values. Basically, an example like this:

could be expressed as:

pub fn wrap(_: Self, comptime handler: fn () void) *const fn (c_int) callconv(.C) void {
    return fn f(_: c_int) callconv(.C) void {
        handler();
        return;
    };
}

if fn literals were a thing in Zig.

3 Likes

Ah this makes sense. I was having some trouble understanding what Sigaction was due to the lack of documentation. This helps, thank you!

This makes sense. I come from more functional-esq langs like Julia so I suppose I was trying to apply those ideas here. Thank you for the ideas, I’ll try your method out as well

1 Like