Taking a writer parameter versus returning a string

I’m relatively new to Zig, and I was wondering what the best practice is to return a string from a function. For example, I have the following function:

inline fn ram(buf: []u8, mem: f32, unit: *const [3]u8) ![]const u8 {
    return if (mem < 10)
        std.fmt.bufPrint(buf, "{d:.2} {s}", .{mem, unit})
    else if (mem < 100)
        std.fmt.bufPrint(buf, "{d:.1} {s}", .{mem, unit})
    else
        std.fmt.bufPrint(buf, "{d:.0} {s}", .{mem, unit});
}

I had to make it inline because it is being called by a function that calls this function, and it results in garbage memory if I don’t use inline. Is it better to instead make the function take a writer parameter?

fn ram(writer: anytype, mem: f32, unit: *const [3]u8) !void {
    return if (mem < 10)
        writer.print("{d:.2} {s}", .{mem, unit})
    else if (mem < 100)
        writer.print("{d:.1} {s}", .{mem, unit})
    else
        writer.print("{d:.0} {s}", .{mem, unit});
}

The reason I opted for the first approach is that sometimes I wanted to have the buffer declared by the callee instead of the caller. Is this a bad practice though?

If inline is effecting the results if your program, you are likely returning a pointer to temporary stack memory somewhere.

You cannot return a pointer to stack memory because the next function will reuse that portion of the stack once that temporary variable goes out of scope.

Perhaps you are attempting to return a slice to some local buf?

See Pointers to Temporary Memory

Regarding taking a writer as a parameter, I would avoid it as much as possible and use concrete types (slices) as much as possible. It will make it easier to track down and itemize your errors.

Each time you use writer as a paramater, you are making it harder for yourself to define the error set returned from the function. This can grow out of hand pretty quickly. Writer parameter has its place for sure, but try to avoid as long as possible.

Your approach with taking the output buffer as a parameter I think is idiomatic and a good start.

fyi, it seems like you might be trying to implement std.fmt.fmtIntSizeBin.

Perhaps you are attempting to return a slice to some local buf?

You’re right, that’s what caused the bug. I had a function that calls this function which bad a local buf variable instead of taking it in as a parameter. After changing that, it works and I don’t need to use inline. Thank you!

fyi, it seems like you might be trying to implement std.fmt.fmtIntSizeBin.

Thanks, I’ll look into that,

The next best option to passing an ‘out-slice’ into the function is probably to pass an allocator, and then use std.fmt.allocPrint() - with the idea that the passed in allocator doesn’t have ‘static lifetime’ but scoped to a lifetime that’s safe for the returned string data but not much longer.

This also delegates the ownership problem to the caller, but is a bit less footgun-y than passing in a slice which might point to transient stack memory.

FWIW proper escape-analysis (which should just produce a compile error instead of trying to be fancy like Go) would be the one big thing I’d want before 1.0

The best practice is to accept a std.mem.Allocator as an argument, allocate memory from the given allocator, then return a pointer to the allocated memory (where you’ve written the result to).

If the caller to your function has a rough idea of how long the string is going to be, it can always pass a StackFallbackAllocator such that in normal operation, the result would be written to the stack and no allocation off the heap would happen at all.

1 Like