GitHub - chung-leong/zigft: Zig function transform library

16 Likes

This is sophisticated stuff @chung-leong. Impressive.

4 Likes

I think some of my code belongs in the “most cursed Zig code” thread :-p

I’ve just add a new close() function for creating closures. I think this is relatively clean syntax:

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

pub fn main() !void {
    var funcs: [5]*const fn (i32) void = undefined;
    for (&funcs, 0..) |*ptr, index|
        ptr.* = try fn_binding.close(struct {
            number: usize,

            pub fn print(self: @This(), arg: i32) void {
                std.debug.print("Hello: {d}, {d}\n", .{ self.number, arg });
            }
        }, .{ .number = index });
    defer for (funcs) |f| fn_binding.unbind(f);
    for (funcs) |f| f(123);
}
Hello: 0, 123
Hello: 1, 123
Hello: 2, 123
Hello: 3, 123
Hello: 4, 123
9 Likes

This is pretty freaking awesome.

2 Likes

The comptime version of .close() makes sense, and I see how a runtime version could make an interface/virtual function/abstract base class.

This sure looks like you made it work at runtime as a *const fn (i32) void. That is amazing!

I’ve looked a few times and I’m still not able to follow how it works. I may not know enough to understand, I see the assembly code in there. Can I ask though, how this works?

1 Like

For every function you’re binding to (or function type if you’re passing a function pointer), a “trampoline” is generated at comptime that merge the bound variables, stored in a Tupperware, with the received arguments to form the correct argument list for the original function. For example, if the original function has 3 arguments and the last one is bound, then trampoline would load a single value from the tuple’s address and place it in the register for the 3rd argument (r8 on x86-64). The first two arguments would already be in the right registers.

Now comes the tricky part, how you you provide the address of the tuple to the trampoline? How do you pass an argument without using the normal argument passing mechanism? The answer is through the stack. In the trampoline there’s a variable that’s deliberately left uninitiated. Assembly code asks for the address of this variable although it doesn’t do anything with it. Just three no-ops serving as a marker in the instruction stream.

Dynamically generated code writes the address of the tuple into this variable inside the trampoline just before it jumps into the trampoline. To determine the correct offset from the stack pointer I basically implemented a partial interpreter for each CPU architecture.

5 Likes

Thank you for explaining this, it’s enlightening at a level that I haven’t worked with before.

1 Like

It’s very much an ugly hack. Ideally, we should be able to associate a local variable with a particular CPU register, like this in C (gcc only):

register void *ptr asm ("rax");