std.fmt.bufPrint() vs std.Io.Writer.print() for formatting data

See title. What considerations should be kept in mind when deciding between these two methods of formatting data?

I’m working on hobby project to simplify using ANSI escape sequences for fancy terminal output. Some ANSI sequences are just const values, but others are parameterized (e.g. RGB colors); for these, I have functions which use std.fmt.bufPrint() to properly format the ANSI sequences, which can then be printed to the terminal.

Example ANSI RGB transformation to get reddish foreground text:
rgb(200, 50, 78) -> "\x1b[38;2;200;50;78m"

To accomplish this, the caller must pass a buffer to use with std.fmt.bufPrint(), which in turn uses the buffer to print via a .fixed std.Io.Writer:

pub fn bufPrint(buf: []u8, comptime fmt: []const u8, args: anytype) BufPrintError![]u8 {
    var w: Writer = .fixed(buf);
    w.print(fmt, args) catch |err| switch (err) {
        error.WriteFailed => return error.NoSpaceLeft,
    };
    return w.buffered();
}

From what I understand, this is the correct use case for bufPrint(), which seems like a convenience function when you already know the max buffer len and want to store the result outside of a Writer. However, I’m wondering if passing a Writer around would be preferable, more flexible, etc over passing a buffer. Here are some specifics I’d like clarified:

  • Either way the caller can decide whether to use stack or heap (either heap-allocate the buffer or use Writer.Allocating), but the Writer version is more flexible if resizing is necessary?
  • Since stdout uses a Writer, would it make sense to cut out the middleman and just pass stdout?

Any insight is appreciated!

If you’re going to write the formatted data to a writer that you already have, or will get later, you should probably just format to that writer directly.

That is a general rule, which applies quite well to what you’re doing. I would find it more ergonomic than what you were doing before.

The reasoning is that, yes, it is more flexible. It also does less work overall, so you might see a very small performance improvement, though it’d probably blend in with the noise.

3 Likes

I think I would just implement a bunch of types with format functions and leave it up to the user to invoke those functions directly or indirectly (via print with format {f}) with whatever writer they want to use.

Constructing a writer via .fixed with a user given buffer seems more complicated and constraining, then just letting the user pass any kind of *std.Io.Writer. (including a fixed writer if they want to)

I think using std.Io.Writer.print() is preferable, because it doesn’t make unnecessary decisions on behalf of the user, without knowing what the user needs. With the writer, the user has the flexibility to use whatever matches their use case.

3 Likes

No, that way your library makes unnecessary constraining decisions.

What if the user wants to print their ascii output to some file, or send it over the network, or accumulate it in a Writer.Allocating to then have it rendered in a self-implemented terminal emulator that runs in-process?

If your library is just about formatting things then it shouldn’t even need to be aware of buffers or stdout, just let the user decide.

4 Likes