Function casting

Just an idea. It seems to me that Zig needs a built-in function that does this:

fn hello(n1: i32, n2: i32) void {
    _ = n2;
    _ = n1;
}
fn world(args: std.meta.ArgsTuple(@TypeOf(hello))) void {
    _ = args;
}

const func: @TypeOf(hello) = @fnCast(world);

The idea here is that we can easily generate a function of the form world at comptime. In actual usage though the form we want is hello.

I’m grabbing the function type from a defined function here for brevity sake. The type is expected to be comptime generated too.

I am using made up syntax here ... but do you basically want this?:

fn world(...args: std.meta.ArgsTuple(@TypeOf(hello))) void {
    _ = args;
}

world(10, 20);

I also had a case where I was thinking it would be cool. I kind of suspect that allowing something like this might create a bunch of complexity and maybe that is why there currently isn’t such a feature.

I thought about it and was beginning to write a rant about many possibilities, then I thought “there is probably an issue about this” here it is Syntax Sugar for tuple arguments, it mentions some of the things I was thinking about and also some problems, I only barely had suspected.

I think we should include the discussion from that issue so that we don’t reinvent everything that was already discovered there. Does that issue match your goals, or do you want something different?

Function casting like this is possible with function pointers:

const func: *const @TypeOf(hello) = @ptrCast(&world);

However you’ll quickly notice that the function signatures are incompatible.
Zig is allowed to pass function arguments by reference: Documentation - The Zig Programming Language
And right now it passes all structs/tuples by reference. So when doing the cast here, you cast two i32 into a pointer to two i32, which will produce a segmentation fault.

2 Likes

Well, sort of. Perhaps an example showing the feature in a realistic usage scenario would explain better:

const std = @import("std");

fn NewFnType(comptime OFT: type) type {
    var info = @typeInfo(OFT);
    // change info in some way...
    return @Type(info);
}

fn transform(comptime oldFn: anytype) NewFnType(@TypeOf(oldFn)) {
    const OFT = @TypeOf(oldFn);
    const NFT = NewFnType(OFT);
    const NFRT = @typeInfo(NFT).Fn.return_type orelse @compileError("Cannot transform");
    const ns = struct {
        fn newFn(newFnArgs: std.meta.ArgsTuple(NFT)) NFRT {
            var oldFnArgs: std.meta.ArgsTuple(OFT) = undefined;
            // copy from one tuple to the other
            const oldRet = @call(.auto, oldFn, oldFnArgs);
            return derive: {
                // do something with oldRet...
            };
        }
    };
    return @fnCast(ns.newFn);
}

Suppose the transform is very simple. All it does is change the calling convention to .C. The following:

fn hello(arg0: i32, arg1:i32) void {
  // ...
}
const world = transform(hello);

should result in a hidden function like this being produced:

fn fnCast0000(arg0: i32, arg1: i32) void {
    return @call(.auto, newFn, .{ arg0, arg1 });
}
2 Likes

It just occurred to me that we can sort of do this already. Since the only thing that varies is the number of arguments and that almost never goes above 10, we just have a fixed list of conversion functions. A matter of pressing Ctrl-V a bunch of times.

Here’s what I came up with so far:

// fn-cast.zig
const std = @import("std");

pub fn fnCast(comptime OutFnT: type, comptime in_fn: anytype) OutFnT {
    const helpers = struct {
        fn fnInfo(comptime FT: type) std.builtin.Type.Fn {
            return switch (@typeInfo(FT)) {
                .Fn => |fi| fi,
                else => @compileError("Not a function"),
            };
        }

        fn ReturnType(comptime FT: type) type {
            return fnInfo(FT).return_type orelse @compileError("Missing return type");
        }

        fn isTuple(comptime T: type) bool {
            return switch (@typeInfo(T)) {
                .Struct => |st| st.is_tuple,
                else => false,
            };
        }

        fn argumentKind(comptime FT: type) []const u8 {
            const f = fnInfo(FT);
            return check: {
                if (f.params.len == 1) {
                    const PT = f.params[0].type orelse @compileError("Missing argument type");
                    if (isTuple(PT)) {
                        break :check "tuple";
                    } else {
                        break :check "1 argument";
                    }
                } else {
                    break :check std.fmt.comptimePrint("{d} arguments", .{f.params.len});
                }
            };
        }

        const InFnT = @TypeOf(in_fn);
        const InFnRT = ReturnType(InFnT);
        const OutFnRT = ReturnType(OutFnT);
        const out_fn_conv = fnInfo(OutFnT).calling_convention;

        fn OutFnPT(comptime i: comptime_int) type {
            return fnInfo(OutFnT).params[i].type orelse @compileError("Missing argument type");
        }

        const converters = struct {
            fn @"tuple to 0 arguments"() OutFnT {
                const ns = struct {
                    fn outFn() callconv(out_fn_conv) OutFnRT {
                        return in_fn(.{});
                    }
                };
                return ns.outFn;
            }

            fn @"tuple to 1 argument"() OutFnT {
                const ns = struct {
                    fn outFn(arg0: OutFnPT(0)) callconv(out_fn_conv) OutFnRT {
                        return in_fn(.{arg0});
                    }
                };
                return ns.outFn;
            }

            fn @"tuple to 2 arguments"() OutFnT {
                const ns = struct {
                    fn outFn(
                        arg0: OutFnPT(0),
                        arg1: OutFnPT(1),
                    ) callconv(out_fn_conv) OutFnRT {
                        return in_fn(.{
                            arg0,
                            arg1,
                        });
                    }
                };
                return ns.outFn;
            }

            // ...etc
        };

        fn convert() OutFnT {
            const in_arg = argumentKind(InFnT);
            const out_arg = argumentKind(OutFnT);
            const converter_name = in_arg ++ " to " ++ out_arg;
            if (!@hasDecl(converters, converter_name)) {
                @compileError("Cannot convert a function with " ++ in_arg ++ " to a function with " ++ out_arg);
            }
            const converter = @field(converters, converter_name);
            return converter();
        }
    };
    return helpers.convert();
}
// test.zig
const std = @import("std");
const fnCast = @import("./fn-cast.zig").fnCast;

fn CFn(comptime InFT: type) type {
    var fnInfo = @typeInfo(InFT).Fn;
    fnInfo.calling_convention = .C;
    return @Type(.{ .Fn = fnInfo });
}

fn toCFn(comptime in_fn: anytype) CFn(@TypeOf(in_fn)) {
    const InFnT = @TypeOf(in_fn);
    const OutFnT = CFn(InFnT);
    const OutFnRT = @typeInfo(OutFnT).Fn.return_type orelse @compileError("Cannot transform");
    const ns = struct {
        fn cFn(out_fn_args: std.meta.ArgsTuple(OutFnT)) OutFnRT {
            var in_fn_args: std.meta.ArgsTuple(InFnT) = undefined;
            inline for (out_fn_args, 0..) |out_fn_arg, index| {
                in_fn_args[index] = out_fn_arg;
            }
            return @call(.auto, in_fn, in_fn_args);
        }
    };
    return fnCast(OutFnT, ns.cFn);
}

fn hello(i: u32, j: f64) void {
    std.debug.print("i = {d}, j = {d}\n", .{ i, j });
}

const world = toCFn(hello);

pub fn main() void {
    world(1234, 3.14);
    std.debug.print("{any}\n", .{@typeInfo(@TypeOf(world)).Fn.calling_convention});
}
1 Like