Serial Log System
In the Logging chapter, we enabled serial output. In this chapter, we will implement Zig’s logging system using that serial output. Essentially, this will be the same as what we did with Surtr’s logging output. So, this chapter will be over in no time. Great, you can get to bed early today.
important
The source code for this branch is in whiz-ymir-serial_logsystem
branch.
Table of Contents
Overriding Default Implementation
First, let's define all the necessary structs and functions at once:
const Writer = std.io.Writer(
void,
LogError,
write,
);
pub const default_log_options = std.Options{
.log_level = switch (option.log_level) {
.debug => .debug,
.info => .info,
.warn => .warn,
.err => .err,
},
.logFn = log,
};
fn log(
comptime level: stdlog.Level,
comptime scope: @Type(.EnumLiteral),
comptime fmt: []const u8,
args: anytype,
) void {
const level_str = comptime switch (level) {
.debug => "[DEBUG]",
.info => "[INFO ]",
.warn => "[WARN ]",
.err => "[ERROR]",
};
const scope_str = if (@tagName(scope).len <= 7) b: {
break :b std.fmt.comptimePrint("{s: <7} | ", .{@tagName(scope)});
} else b: {
break :b std.fmt.comptimePrint("{s: <7}-| ", .{@tagName(scope)[0..7]});
};
std.fmt.format(
Writer{ .context = {} },
level_str ++ " " ++ scope_str ++ fmt ++ "\n",
args,
) catch {};
}
As with Surtr, we define default_log_options
to override the default std_options
. The log_level
can be specified at build time, and logFn
is set to the log
function. The log()
function is basically the same as Surtr’s. One difference is that the scope string is limited to a maximum length of 7 characters: if it’s shorter, it’s padded with spaces; if longer, it’s truncated with -
.
Add the following to build.zig
to use option
module:
ymir_module.addOptions("option", options);
ymir.root_module.addOptions("option", options);
To make ymir/log.zig
available from main.zig
, export it from ymir/ymir.zig
. Since exporting it as log
could cause confusion with std.log
, export it as klog
instead:
pub const klog = @import("log.zig");
Override the default values by using the defined default_log_options
:
const klog = ymir.klog;
pub const std_options = klog.default_log_options;
Initializing Serial
This logging system depends entirely on serial output. Therefore, you need to initialize the serial port first, then pass the Serial
instance to the logging system for initialization:
const sr = serial.init();
klog.init(sr);
log.info("Booting Ymir...", .{});
The passed Serial
instance is stored in a variable inside log.zig
and used for output during logging:
var serial: Serial = undefined;
pub fn init(ser: Serial) void {
serial = ser;
}
fn write(_: void, bytes: []const u8) LogError!usize {
serial.writeString(bytes);
return bytes.len;
}
Summary
With this, the setup for serial output is complete. From now on, you can use serial log output like std.log.info()
in any file without needing to import ymir/log.zig
. Convenient, isn’t it? When you run it, you should see the scope and log level output together as shown below:
[INFO ] main | Booting Ymir...
In the Booting Kernel chapter, we validated the BootInfo
argument from Surtr. At that time, since we didn't have a logging system set up, we silently returned on validation failure. Now that logging is available, let's enable error output as shown below:
validateBootInfo(boot_info) catch {
log.err("Invalid boot info", .{});
return error.InvalidBootInfo;
};
In this chapter, we implemented Zig’s logging system using serial output. From now on, you can use Zig’s logging system without worrying about the serial output working behind the scenes. With this, the groundwork for implementing Ymir is complete. Starting from the next chapter, we will begin replacing various data structures provided by UEFI with Ymir’s own implementation.