Hi, I’m compiling some bare metal code for RISC-V something like this (a bit cut down from my real code):
const std = @import("std");
const Target = @import("std").Target;
// Construct the build graph in `b` (this doesn't actually build anything itself).
pub fn build(b: *std.Build) void {
const features = Target.riscv.Feature;
var cpu_features = Target.Cpu.Feature.Set.empty;
cpu_features.addFeature(@intFromEnum(features.i));
cpu_features.addFeature(@intFromEnum(features.m));
cpu_features.addFeature(@intFromEnum(features.a));
cpu_features.addFeature(@intFromEnum(features.f));
cpu_features.addFeature(@intFromEnum(features.zifencei));
cpu_features.addFeature(@intFromEnum(features.zicsr));
// cpu_features.addFeature(@intFromEnum(features.d));
const target = b.resolveTargetQuery(.{
.cpu_arch = .riscv32,
// Note, this is really the default Environment (usually the
// 4th component of the target triple). Calling it the ABI is wrong.
.abi = .none,
.os_tag = .freestanding,
.cpu_features_add = cpu_features,
});
const optimize = b.standardOptimizeOption(.{});
// Module for runtime support (crt0.S etc).
const runtime_mod = b.createModule(.{
// `root_source_file` is the Zig "entry point" of the module. If a module
// only contains e.g. external object files, you can make this `null`.
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = b.path("src/runtime.zig"),
.target = target,
.optimize = optimize,
});
runtime_mod.addCSourceFile(.{ .file = b.path("src/crt0.S") });
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/c_test_root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
exe_mod.addCSourceFile(.{ .file = b.path("src/my_test.c") });
exe_mod.addImport("runtime", runtime_mod);
const exe = b.addExecutable(.{
.name = "my_test",
.root_module = exe_mod,
});
exe.setLinkerScript(b.path("src/link.ld"));
b.installArtifact(exe);
}
This works, except if I try to #include <stdio.h>
(or other standard headers) in my C file. Then I get:
/.../my_test.c:3:10: error: 'stdio.h' file not found
#include <stdio.h>
^~~~~~~~~~
If you do this with the normal RISC-V GCC it works fine, I assume because it provides Newlib as a C library for bare metal applications. It seems like Newlib assumes the Linux syscall ABI so it will happily compile printf("");
and you’ll end up with ecall
s to the Linux write()
syscall using the Linux syscall numbers. Bit weird but whatever - it works fine and you can implement whatever syscalls you need in crt0.S
(or I think you could use Proxy Kernel but I’ve never tried).
Anyway, I guess Zig is doing the “correct” thing here and not pretending there’s a libc when there isn’t one… but that’s quite annoying because it means you can’t even use functions like sprintf()
which actually work fine without any support from the execution environment.
Is there any way around this?