Best practices writing to stdout

From other languages I’m not too familiar around managing a handle to stdout throughout my program, as that is usually done for you in one way or another. Since stdout in Zig is explicitly considered a resource, I feel I need to be aware how and when this resource is used throughout. Say I’m writing a library and there’s a function that prints stuff:

const std = @import("std");

pub fn printStuff() !void {
    const outw = std.io.getStdOut().writer();
    try outw.print("Stuff\n", .{});
}

Is the above fine to do, or should I make sure to lock stdout first before writing to it incase the library is used in a multi-threaded application? It feels like there are potential foot-guns here. Is it good style to instead always propagate the output-stream choice to the user similar to how we do with allocators in Zig:

const std = @import("std");

pub fn printStuff(out_stream: std.fs.File) !void {
    const outw = out_stream.writer();
    try outw.print("Stuff\n", .{});
}

This however adds a slight mental overhead to using the library, especially if having a single stdout-handle at the top of the callstack that you pass around isn’t usual in Zig, unlike allocators.

How would you manage printing to stdout in your library? Or alternatively, how do you expect a library you are using to handle outputting to stdout?

I put this in the explain category because we’re talking about best practices and your question at the end asks users about how they would handle things in their own code. Help is more like “why won’t this compile”, “I’m getting an invalid value”, or “what does this error message mean.”

2 Likes

Sorry, I didn’t see you moving it the first time. I thought I messed up and put posted in the wrong category!

1 Like

Loris Cro “Advanced Hello World” is the best treatise on this topic.

4 Likes

I watched the entire video, it was great! I think a main takeaway for me is that if you are going to do some printing further down in the callstack, you should always lock stdout before doing so. If the caller wants to do away with the locking overhead they can explicitly pass -fsingle-threaded when compiling.

I personally would not want libraries to randomly print stuff to stdout. I want more control over what gets printed and where it gets printed. For example for the game I’m working on, I want to log all messages to a file, so it’s easier to debug when the game is executed on a user’s machine.

In C the general solution for this seems to be that each library has their own debug callback that needs to be defined by the user.

But in Zig we do have a better solution: The logging interface. You would then write messages like this:

std.log.scoped(.yourLibName).debug("debug msg", .{});
std.log.scoped(.yourLibName).info("info msg", .{});
std.log.scoped(.yourLibName).warn("warning", .{});
std.log.scoped(.yourLibName).err("error msg", .{});

The great thing about this is that you don’t need to worry about the details at every call site. You don’t need to ensure that the mutex is locked everywhere.
Additionally this API allows the user to intercept your log messages by overwriting the default log function in their root file:


pub const std_options = struct {
	pub const log_level = .debug;
	pub fn logFn(
		comptime level: std.log.Level,
		comptime scope: @Type(.EnumLiteral),
		comptime format: []const u8,
		args: anytype,
	) void {
		if(scope == .yourLibName and level == .debug) return; // Could for example filter the debug spam of your library
		// The user can decide whether to print to stdout/stderr or to a file
	}
}
9 Likes

Yeah std.log seems like a no-brainer for anything logging related, I’m glad the std includes that functionality. In my case though the part I’m working on is pretty-print functionality printing formatted output that includes color codes specifically meant for the terminal. The user calls a prettyPrint function explicitly so it’s not random logging throughout, so I think in my case it makes sense to do a buffered mutexed writer to stdout.

If the application is not multi-threaded, the mutex might not be necessary. Also, another gotcha I experienced was that if some part of the application is printing to stdout and another part to stderr, those can get mixed together - not exactly pretty. And mutex-protecting stdout doesn’t help in this case.

If the application is not multi-threaded, the mutex might not be necessary.

The nice thing either way is that by passing -fsingle-threaded as a compile option all mutexes become empty structures with no-ops, meaning the user can always opt-out of the mutex overhead.

Do you mean mixed together as in out-of-order intermingled? Or straight up data-raced output? I guess that makes sense though, you’d need a lock that covers both stdout and stderr to avoid that…?

1 Like

I’d say “racing output”, just messed up. Ok I admit, if you encounter such a situation, something else might be messed up in general, like debug print output scattered around the code :wink:

As far as “best practices” are concerned here, if this were a simple console app I would agree that you don’t need a mutex lock here in this context. But I would also say that I think it’s better that you declare your writer in the main function and declare your function as something like printStuff(writer: anytype, stuff: []const u8). I would also take this a step further and say that you don’t need a BufferedWriter in this case as well. Ultimately whether you do thread locks or using a BufferedWriter would depend on how you implement your library. You may end up using it or not using it but what’s more important here is that using the basic writer.print method will always work in most cases. So you have the benefit of keeping your code simple and you can decide later whether or not you really need those extra capabilities, when your library gets more complex. Also say if you do go ahead and used the BufferedWriter. Down the line you might want to check to see if there is some issue there caused by the buffering. That would be a lot of flush statements you need to comment out, just to find out it didn’t make a difference. So it would make more sense to convert that writer to a BufferedWriter say as part of a release build, when you already have everything else worked out. So if for some reason there is an issue there it would be easy to isolate.

Edit: I also agree that std.log is better for a library. stdout should be reserved for the program calling the library. so in this case i don’t think using a buffered writer is an option, but the same logic would apply for the mutex lock. Depending on what kind of library you are writing, it’s likely that it will only be single threaded so it would add a lot of unecessary complexity.

3 Likes

What if you only call the function of your library to format the output and print it in the caller function?

3 Likes