# Function signature that uses variadic args as part of the interface vtable

**URL:** <https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094>\
**Category:** Help\
**Tags:** language, comptime\
**Created:** [August 5, 2026, 5:11pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094 "2026-08-05T17:11:51Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![cciva](https://ziggit.dev/letter_avatar_proxy/v4/letter/c/71c47a/32.png) [@cciva](https://ziggit.dev/u/cciva)\
**Post date:** [August 5, 2026, 5:11pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/1 "2026-08-05T17:11:51Z")

</div>

Hi all,

Newbie here, I’m currently trying out/ learning zig, so please bear with me. This might have been answered before, but I couldn’t find anything related to my specific scenario.

As an exercise (interfaces, comptime etc.), I’m trying to create small logging facility. There is a single interface `LogTarget` which can be implemented by various log sinks, e.g. file, stdout, syslog etc. And it looks like this:

```zig
/// This is interface representing log target (e.g. file, std etc.)
pub const LogTarget = struct {
  impl: *anyopaque,
  vtable: *const VTable,

  // vtable for dispatching
  const VTable = struct {
    // should be implemented by log target
    op_write: *const fn(*anyopaque, Level, comptime []const u8, args: anytype) anyerror!void,
  };

  fn init(from: anytype) LogTarget {
    const T = @TypeOf(from);

    const proxy = struct {
      pub fn write(ptr: *anyopaque, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
        try unwrap(ptr, T).write(lvl, fmt, args);
      }
    };

    return .{
      .impl = from,
      .vtable = &VTable{
        .op_write = proxy.write
      }
    };
  }

  pub fn write(self: LogTarget, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
    try self.vtable.op_write(self.impl, lvl, fmt, args);
  }
};

```

Now, I have another type `Logger` which can have multiple log targets and each one of them should invoke `write` when I try to log something. Here is how `Logger` type looks like:

```zig
pub const Logger = struct {
  alloc: Alloc,
  targets: std.ArrayList(LogTarget),

  pub fn create(alloc: Alloc, name: []const u8) Logger {
    return .{
      .alloc = alloc,
      .targets = .empty,
    };
  }

  // register log target
  pub fn register(self: *Logger, target: anytype) !void {
    try self.targets.append(
      self.alloc, 
      LogTarget.init(target)
    );
  }

  // log to all registered targets
  pub fn log(self: Logger, lvl: Level, comptime fmt: []const u8, args: anytype) !void {
    for (self.targets.items) |target| {
      try target.write(lvl, fmt, args);
    }
  }

  pub fn deinit(self: *Logger) void {
    self.targets.deinit(self.alloc);
  }
};

```

One possible implementations of the `LogTarget`:

```zig
// simple stdout target
pub const Console = struct {
  level: Level = .debug,

  // 
  pub fn write(self: *Console, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
    _ = self;
    _ = args;

    // I would like to be able to use 'args' passed from Logger to construct a message
    // and then manipulate further prior to writing to stdout
    std.debug.print("[{s}] - {s}\n", .{ lvl.str(), fmt });
  }
};

```

Here is a simple test:

```zig
test "logger:noop" {
  const allocator = std.testing.allocator;

  var logger = Logger.create(allocator, "noop-logger");
  defer logger.deinit();

  try logger.log(.info, "we shouldn't see this", .{});
}

```

Problem arises when I change `LogTarget` interface and add `args:anytype` argument to the `LogTarget.write` function. I would like to be able to forward all args from `Logger.log` to `LogTarget.write` (implementation), but when I try to do so, compiler yells at me:

```zig
[2026-08-05T16:08:27.585Z] Running test: logger.zig - logger:noop
Command failed: /home/civa/.asdf/shims/zig test --test-filter logger:noop /home/civa/dev/projects/zig/demo/src/lib/logger.zig
src/lib/logger.zig:109:7: error: variable of type 'logger.Logger' must be const or comptime
  var logger = Logger.create(allocator, "noop-logger");
      ^ ~~~~~
src/lib/logger.zig:62:25: note: struct requires comptime because of this field
  targets: std.ArrayList(LogTarget),
           ~~~~~~~~~~~~~^~~~~~~~~~~
/home/civa/.asdf/installs/zig/0.15.1/lib/std/array_list.zig:615:16: note: struct requires comptime because of this field
        items: Slice = &[_]T{},
               ^ ~~~~
src/lib/logger.zig:31:11: note: struct requires comptime because of this field
  vtable: *const VTable,
          ^ ~~~~~~~~~~~~
src/lib/logger.zig:34:15: note: struct requires comptime because of this field
    op_write: *const fn(*anyopaque, Level, comptime []const u8, args: anytype) anyerror!void,
              ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/lib/logger.zig:34:15: note: function is generic

```

Now, I can’t say I fully understand entire compiler error, but I know for sure `anytype` must be `comptime`. I cant wrap my head around how to combine `comptime` with function pointers inside vtable (is it possible?) and should `LogTarget` interface become `comptime` entirely?

Sorry for the long post and thanks for the help.

civa

---

<div class="post-metadata">

**Author:** ![SillyProgramming](https://ziggit.dev/user_avatar/ziggit.dev/sillyprogramming/32/8444_2.png) [@SillyProgramming](https://ziggit.dev/u/SillyProgramming)\
**Post date:** [August 5, 2026, 5:29pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/2 "2026-08-05T17:29:38Z")

</div>

Consider this, when the compiler has a anytype as a parameter, it generates the code neeeed for the code at comprime.  
So if you are using a a VTable it does not what function it may be and generate the he expected code for each type.

---

<div class="post-metadata">

**Author:** ![cciva](https://ziggit.dev/letter_avatar_proxy/v4/letter/c/71c47a/32.png) [@cciva](https://ziggit.dev/u/cciva)\
**Post date:** [August 5, 2026, 5:51pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/3 "2026-08-05T17:51:10Z")

</div>

Yep, I’m aware of that. It was just my understanding (or rather lack of) that variadic `args` (tuple?) could be emulated with `anytype` like in `pub fn print(comptime fmt: []const u8, args: anytype) void`.

---

<div class="post-metadata">

**Author:** ![splinterofchaos](https://ziggit.dev/user_avatar/ziggit.dev/splinterofchaos/32/9375_2.png) [@splinterofchaos](https://ziggit.dev/u/splinterofchaos)\
**Post date:** [August 5, 2026, 6:10pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/4 "2026-08-05T18:10:28Z")

</div>

(Note: I’m still learning zig myself so take this with a grain of salt.)

That’s correct, but the issue is that your vtable contains a function pointer to a function whose arguments are unresolved. Normally, the type of `anytype` is resolved at the call site based on the arguments, but here it’s just a function pointer so type resolution has to happen independently. When I copied over the code (slightly edited because some code is missing) and ran it, I got this:

```zig
test.zig:33:24: error: unable to resolve comptime value
        try self.vtable.op_write(self.impl, lvl, fmt, args);
            ~~~~~~~~~~~^~~~~~~~~

```

Note that `self.vtable.op_write` is all that’s highlighted. When the compiler sees a reference to the variable, it tries to resolve the type, but it can’t, and it can’t even use the local context.

But there’s an easy fix. Do all formatting _before_ selecting the target so the `LogTarget` can just accept a slice representing what it should print.

---

<div class="post-metadata">

**Author:** ![gyozo](https://ziggit.dev/letter_avatar_proxy/v4/letter/g/97f17d/32.png) [@gyozo](https://ziggit.dev/u/gyozo)\
**Post date:** [August 5, 2026, 6:28pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/5 "2026-08-05T18:28:12Z")

</div>

You can also “compile” the format string and the arguments into a type-erased representation, and that can now be passed to a virtual function.

```zig
const RuntimeFormatArg = struct {
    data: *const anyopaque,
    format_string: *const u8,
    type: *const RuntimeTypeInfo,
};
fn printImpl(args: []const RuntimeFormatArg) void;

```

You’ll need to come up with a useful RuntimeTypeInfo though (it can be just an enum or even a vtable-like object itself).

Something like [https://en.cppreference.com/cpp/utility/format/vformat](https://en.cppreference.com/cpp/utility/format/vformat)

---

<div class="post-metadata">

**Author:** ![cciva](https://ziggit.dev/letter_avatar_proxy/v4/letter/c/71c47a/32.png) [@cciva](https://ziggit.dev/u/cciva)\
**Post date:** [August 5, 2026, 6:28pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/6 "2026-08-05T18:28:54Z")

</div>

Hi @splinterofchaos and thanks for taking the time to look into it and the explanation - it makes sense.

> Do all formatting _before_ selecting the target so the `LogTarget` can just accept a slice representing what it should print.

Yes, that was my initial plan, but I really wanted each `LogTarget` to has its own formatting options.

---

<div class="post-metadata">

**Author:** ![cciva](https://ziggit.dev/letter_avatar_proxy/v4/letter/c/71c47a/32.png) [@cciva](https://ziggit.dev/u/cciva)\
**Post date:** [August 5, 2026, 6:33pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/7 "2026-08-05T18:33:48Z")

</div>

Thanks @gyozo actually I had an idea to use something like this:

```zig
const Val = union(enum) {
  str: []const u8,
  num: i64,
  ....
}

```

but had given up and just tried to slap `anytype` which obviously wasn’t good idea. Thanks for the help, will go from there and see what happens.

---

<div class="post-metadata">

**Author:** ![gyozo](https://ziggit.dev/letter_avatar_proxy/v4/letter/g/97f17d/32.png) [@gyozo](https://ziggit.dev/u/gyozo)\
**Post date:** [August 5, 2026, 6:58pm UTC](https://ziggit.dev/t/function-signature-that-uses-variadic-args-as-part-of-the-interface-vtable/17094/8 "2026-08-05T18:58:47Z")

</div>

Your enum might be just good enough. Implementing a generic type-erased solution can be a good learning exercise but in practice I think most of the time you would just print strings and objects that you know or are easy to turn into strings (like ints)
