Generic function call or generic function wrapper

In code that I’m writing, there are needs to doSomeGenericStuff before and after function call. Previously I’ve done it with code generation, but it has drawbacks (reduces compilation speed and makes it feel uncomfortable).
Now I’m thinking, what if I can wrap function with other comptime function.

Let’s say I have function like this:

/// T: type of any function
pub fn doSomeGenericStuff(comptime T: type, args: <something-like-tupple>) @typeInfo(T).@"fn".return_type {
    ... doing stuff before function call ...

    // calling function of type T with arguments from `args`
    // like this (N) known at comptime:
    // T(args[0], ... , args[N]);
    const res = ...;

    ... doing stuff after function call ...

    return res;
}

Is it even possible?

I think @call would fit perfectly. I would try it.

if you are looking for decorator - check std wrapper of non-thread-safe allocator

1 Like

I think, this approach wouldn’t fit my use case. I’m trying to make library, that takes arbitrary exported function and wrap it in two ways:

  • Middleware, that can be dynamically linked instead of actual lib. It must get function arguments, send them to the server and get result back.
  • Server, that would get data from the middleware, process it using defined functions and send back results.

The second language that I’m working with (MQL5), uses propraitary enviroment to run, which forbids any external debuggers, so I can’t examine my Zig code without making some sort of sandbox or this middleware/server approach.

Actually, when I’ll publish this library, I think it can be used in similar situations (outside just MQL5), when debugging is restricted in some way.

Maybe you could use this:

But it is quite advanced and I haven’t played around with it yet in detail.

1 Like

That’s final implementation:

const builtin = @import("builtin");
const std = @import("std");
const posix = std.posix;
const Allocator = std.mem.Allocator;
const AnyReader = std.io.AnyReader;
const AnyWriter = std.io.AnyWriter;

const InternalTypeInfo = @import("InternalType.zig");

pub fn readValue(comptime T: type, reader: AnyReader) !T {
    const it = InternalTypeInfo.init(T);
    return switch (it.type_info) {
        .@"struct" => try reader.readStruct(T),
        else => read_block: {
            const buf = try reader.readBytesNoEof(it.size);
            break :read_block std.mem.bytesToValue(T, &buf);
        },
    };
}

pub fn writeValue(comptime T: type, writer: AnyWriter, value: T) !void {
    const it = InternalTypeInfo.init(T);
    switch (it.type_info) {
        .@"struct" => try writer.writeStruct(value),
        else => try writer.writeAll(&std.mem.toBytes(@as(T, value))),
    }
}

pub fn Input(comptime T: type) type {
    const fn_ti = @typeInfo(T).@"fn";

    var p_len = 0;
    for (fn_ti.params) |p| {
        if (p.type != null) {
            p_len += 1;
        }
    }

    var types: [p_len]type = undefined;
    var i = 0;
    for (fn_ti.params) |p| {
        if (p.type != null) {
            types[i] = p.type.?;
            i += 1;
        }
    }
    return std.meta.Tuple(&types);
}

pub fn callRemote(
    comptime T: type,
    reader: AnyReader,
    writer: AnyWriter,
    args: *Input(T),
) !@typeInfo(T).@"fn".return_type.? {
    const fn_info = @typeInfo(T).@"fn";
    const fn_ret = fn_info.return_type;
    inline for (fn_info.params, 0..) |fn_param, i| {
        const param_info = @typeInfo(fn_param.type.?);
        switch (param_info) {
            .pointer => |p| if (p.size == .many) {
                const len = blk: {
                    const j: usize = comptime len_idx_blk: {
                        for (
                            fn_info.params[i + 1 ..],
                            i + 1..,
                        ) |p_size, j| {
                            if (p_size.type.? == usize)
                                break :len_idx_blk j;
                        }

                        @compileError("Parameter's length not found!");
                    };
                    break :blk args[j];
                };

                try writeValue(usize, writer, len);
                for (args[i][0..len]) |val| {
                    try writeValue(p.child, writer, val);
                }
            } else if (@typeInfo(p.child) == .pointer) {
                @compileError("Parameter can't be pointer to pointer!");
            } else try writeValue(p.child, writer, args[i].*),
            else => {
                try writeValue(fn_param.type.?, writer, args[i]);
            },
        }
    }

    loop: inline for (fn_info.params, 0..) |fn_param, i| {
        const param_info = @typeInfo(fn_param.type.?);
        switch (param_info) {
            .pointer => |p| {
                if (p.is_const) continue :loop;
                if (p.size != .one) {
                    const len = try readValue(usize, reader);
                    for (args[i][0..len]) |*val| {
                        val.* = try readValue(p.child, reader);
                    }
                } else {
                    args[i] = try readValue(p.child, reader);
                }
            },
            else => continue :loop,
        }
    }

    if (fn_ret) |ret| {
        return try readValue(ret, reader);
    }
}

pub fn callLocal(
    comptime T: type,
    @"fn": *const T,
    reader: AnyReader,
    writer: AnyWriter,
    allocator: Allocator,
) !void {
    const fn_info = @typeInfo(T).@"fn";
    const fn_ret = fn_info.return_type;

    var args: Input(T) = undefined;

    loop: inline for (fn_info.params, 0..) |fn_param, i| {
        const param_info = @typeInfo(fn_param.type.?);
        switch (param_info) {
            .pointer => |p| {
                if (p.is_const) continue :loop;
                if (p.size != .one) {
                    const len = try readValue(usize, reader);
                    const buf = try allocator.alloc(p.child, len);
                    args[i] = buf.ptr;
                    for (args[i][0..len]) |*val| {
                        val.* = try readValue(p.child, reader);
                    }
                } else {
                    args[i] = try readValue(p.child, reader);
                }
            },
            else => continue :loop,
        }
    }
    defer {
        loop: inline for (fn_info.params, 0..) |fn_param, i| {
            const param_info = @typeInfo(fn_param.type.?);
            switch (param_info) {
                .pointer => |p| {
                    if (p.is_const) continue :loop;
                    if (p.size != .one) {
                        const len = blk: {
                            const j: usize = comptime len_idx_blk: {
                                for (
                                    fn_info.params[i + 1 ..],
                                    i + 1..,
                                ) |p_size, j| {
                                    if (p_size.type.? == usize)
                                        break :len_idx_blk j;
                                }

                                @compileError("Parameter's length not found!");
                            };
                            break :blk args[j];
                        };
                        allocator.free(args[i][0..len]);
                    }
                },
                else => continue :loop,
            }
        }
    }

    const res = @call(.auto, @"fn", args);

    inline for (fn_info.params, 0..) |fn_param, i| {
        const param_info = @typeInfo(fn_param.type.?);
        switch (param_info) {
            .pointer => |p| if (p.size == .many) {
                const len = blk: {
                    const j: usize = comptime len_idx_blk: {
                        for (
                            fn_info.params[i + 1 ..],
                            i + 1..,
                        ) |p_size, j| {
                            if (p_size.type.? == usize)
                                break :len_idx_blk j;
                        }

                        @compileError("Parameter's length not found!");
                    };
                    break :blk args[j];
                };

                try writeValue(usize, writer, len);
                for (args[i][0..len]) |val| {
                    try writeValue(p.child, writer, val);
                }
            } else if (@typeInfo(p.child) == .pointer) {
                @compileError("Parameter can't be pointer to pointer!");
            } else try writeValue(p.child, writer, args[i].*),
            else => {
                try writeValue(fn_param.type.?, writer, args[i]);
            },
        }
    }

    if (fn_ret) |ret| {
        try writeValue(ret, writer, res);
    }
}

As I’ve thought yet, @call fits there. But I don’t know yet if this is correct approach for doing such type of stuff.
I think it’s hard to use this example in my case:

Since I have unknown amount of arguments… But may be I’m wrong.