In MicroZig, we structure applications similar to build.zig: the user provided code is imported as a module into the root of the application. We originally did this because it very easily allows us to inject whatever code we need before we explicitly call the user’s main(). Depending on the hardware, this can vary greatly, and no, we should not be upstreaming this to the stdlib.
The downside to this approach is that packages who use root declarations to configure themselves at compile-time can no longer do so, as the user’s code is not root. Like the std_options declaration for the standard library. So I’m looking into alternatives, and re-evaluating why we put startup code in a root module to begin with, and I’ve become stumped.
For the standard library, startup code is injected in start.zig, which has a comptime block that exports functions, or does a number of things before main(), depending on the system. This is exactly what MicroZig wants to do, so I tried replicating the behaviour, but I failed, and that’s why I need help.
In this branch I’ve made the user’s application the root of the executable, and in the microzig package I’ve replicated what the standard library does:
- Force import of
start.zigat comptime: std vs MicroZig - Force import of
rootat comptime instart.zig: std vs MicroZig
These are the two things I understand are needed in order to have comptime logic run which sets up startup code. There does still seem to be a difference though, because I need to explictly use the microzig import in order to trigger my startup code, where if I make an empty program that doesn’t even import std, the main function shows up in the executable:
0000000100099734 <_test.main>:
100099734: a9bf7bfd stp x29, x30, [sp, #-0x10]!
100099738: 910003fd mov x29, sp
10009973c: a8c17bfd ldp x29, x30, [sp], #0x10
100099740: d65f03c0 ret
However if I make a blinky program in MicroZig, that actually uses the import! I still need to explicitly “use” it in order to have the comptime startup code setup to evaluate:
const std = @import("std");
const microzig = @import("microzig");
const rp2xxx = microzig.hal;
const time = rp2xxx.time;
// Even though microzig is imported and used in this blinky program, I still
// need to explicitly "use" it. If I don't, I get a linking _warning_ saying
// that I'm missing a symbol that the linkerscript (created by MicroZig)
// expects, and I get an empty program. If I "use" it explicitly, all is fine.
comptime {
_ = microzig;
}
// Our no-op is needed otherwise we get missing posix compile errors -- A
// separate issue.
pub const std_options: std.Options = .{
.logFn = microzig.options.logFn,
};
const pin_config: rp2xxx.pins.GlobalConfiguration = .{
.GPIO25 = .{
.name = "led",
.direction = .out,
},
};
const pins = pin_config.pins();
pub fn main() !void {
pin_config.apply();
while (true) {
pins.led.toggle();
time.sleep_ms(250);
}
}
Is the stdlib specially cased, where its root always gets comptime evaluated regardless of the user importing it? That would explain this behaviour, or it’s something I don’t understand.
