bufPrint: Backing buffer confusion

I’m relatively new to Zig and am working on an ANSI escape sequence module for easy terminal coloring, styling, etc. Basic sequences are just const values, but parameterized sequences (e.g. RGB, cursor movement) are created via functions, which currently use bufPrint to format the output.

Here are my foreground RGB/Hex functions (in a file called color.zig):

pub const csi = "\x1b[";
pub fn rgb(r: u8, g: u8, b: u8) []const u8 {
    var buf: [25]u8 = undefined;
    return std.fmt.bufPrint(&buf, csi ++ "38;2;{d};{d};{d}m", .{ r, g, b }) catch unreachable;
}
pub fn hex(hexcode: u24) []const u8 {
    var buf: [25]u8 = undefined;
    const r: u8 = @intCast((hexcode & 0xff0000) >> 16);
    const g: u8 = @intCast((hexcode & 0x00ff00) >> 8);
    const b: u8 = @intCast(hexcode & 0x0000ff);
    return std.fmt.bufPrint(&buf, csi ++ "38;2;{d};{d};{d}m", .{ r, g, b }) catch unreachable;
}

This works as expected in most cases but acts strange with the following code (in main.zig, rgb and hex fn’s are imported):

const reset = "\x1b[0m";
const rgb = color.fg.rgb(234, 126, 19);
const hex = color.fg.hex(0xea7e13);
try unbuf_stdout.print("{s}rgb{s}\n", .{rgb, reset});
try unbuf_stdout.print("{s}hex{s}\n", .{hex, reset});

Here’s the weird output:
image

If I comment out the rgb or hex lines, the correct foreground coloring is output.

It feels like there’s something about the buffers that I’m failing to understand, is there some overlap happening or something? I’m also open to alternative suggestions if there’s a better way to do this. Thank you!


UPDATE:
Changing the order of the print statements results in expected output:

// This results in expected output
const reset = "\x1b[0m";
const rgb = color.fg.rgb(234, 126, 19);
try unbuf_stdout.print("{s}rgb{s}\n", .{rgb, reset});
const hex = color.fg.hex(0xea7e13);
try unbuf_stdout.print("{s}hex{s}\n", .{hex, reset});
// This results in the same first line as the weird output, 
// but the second line is colored correctly
const reset = "\x1b[0m";
const rgb = color.fg.rgb(234, 126, 19);
const hex = color.fg.hex(0xea7e13);
try unbuf_stdout.print("{s}rgb{s}\n{s}hex{s}\n", .{rgb, reset, hex, reset});

What’s the scoop here? Appreciate the help o7

Hi and welcome to the forum!

Your issue stems from returning a reference to memory allocated on the stack (the buf local variable)

1 Like

Does this mean that the stack for rgb() (and therefore the buffer containing the escape seq) is cleaned up when I call hex()? That would make sense re: the behavior when printing is reordered. Is there a way to do this properly without heap-allocating?

Does this mean that the stack for rgb() (and therefore the buffer containing the escape seq) is cleaned up when I call hex()?

Yeah, referencing memory allocated by a local variable on the stack after the function has returned exhibits undefined behavior.

One way is to pass down a buffer from the caller:

const std = @import("std");

pub const csi = "\x1b[";
pub fn rgb(r: u8, g: u8, b: u8, buf: []u8) ![]const u8 {
    return try std.fmt.bufPrint(buf, csi ++ "38;2;{d};{d};{d}m", .{ r, g, b });
}
pub fn hex(hexcode: u24, buf: []u8) ![]const u8 {
    const r: u8 = @intCast((hexcode & 0xff0000) >> 16);
    const g: u8 = @intCast((hexcode & 0x00ff00) >> 8);
    const b: u8 = @intCast(hexcode & 0x0000ff);
    return try std.fmt.bufPrint(buf, csi ++ "38;2;{d};{d};{d}m", .{ r, g, b });
}

pub fn main() !void {
    var buf: [25]u8 = undefined;
    const reset = "\x1b[0m";
    const _rgb = try rgb(234, 126, 19, &buf);

    var buf2: [25]u8 = undefined;
    const _hex = try hex(0xea7e13, &buf2);
    std.debug.print("{s}rgb{s}\n{s}hex{s}\n", .{ _rgb, reset, _hex, reset });
}
3 Likes

Since I know the max length of a color sequence it would be nice to avoid needing to try my rgb() and hex() methods. Just tried a version with a single global buffer in color.zig and it seems to work as expected. Any pitfalls with this strategy?

This definitely does not work, buffer gets overwritten. I thought it was working b/c I was using equivalent rgb/hex values so the overwrite appeared correct. I shall pass the buffer from the caller as suggested, thank you!

In your second example (under UPDATE in the first post), you actually want to pass down separate buffers to avoid the hex call from overwriting the buffer populated by the rgb call. This is necessary since you print at the end after rgb/hex calls, instead of between. I didn’t notice this as first, since the color was the same.

I updated my example to reflect this.

3 Likes