Intended behavior of module-specific compilation options

I am trying to understand what exactly resolved_target and other compilation options in std.Build.Module mean.

Consider the example at the bottom of this post, where a Linux executable is allowed to import a module targeting WASI.

Questions:

  1. Is it intentional that this import succeeds? I expected an error due to the foreign module specifying an incompatible target.
  2. The foreign module observes a value of builtin.os that does not correspond to the actual target, so any OS-specific code in that module may misbehave. I think this indicates a bug, but I would like to hear from somebody familiar with the build system’s design before reporting it.
  3. resolved_target and other fields of std.Build.Module default to null. Does this mean that the module is, by default, allowed to be used with any target/compilation options? Or does it mean that the native target is inferred?

build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "main",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    const foreign = b.addModule("foreign", .{
        .root_source_file = b.path("src/foreign.zig"),
        .target = b.resolveTargetQuery(.{ .os_tag = .wasi }),
    });
    exe.root_module.addImport("foreign", foreign);
    b.step("run", "Run the executable").dependOn(&b.addRunArtifact(exe).step);
}

src/main.zig

const std = @import("std");

pub fn main() void {
    std.debug.print("native os: {}\n", .{ @import("builtin").os.tag });
    std.debug.print("foreign os: {}\n", .{ @import("foreign").os.tag });
}

src/foreign.zig

pub const os = @import("builtin").os;

Output

$ ~/zig-linux-x86_64-0.13.0/zig build run
native os: Target.Os.Tag.linux
foreign os: Target.Os.Tag.wasi
3 Likes

I think I have answers:

  1. I believe so, builtin is really just giving you information about your target rather than actually interacting with the OS itself. So it’s essentially just feeding back to you what you specified in build.zig.
  2. I actually tried this, and successfully get a compile error:

foreign.zig:

pub const os = @import("builtin").os;
pub const std = @import("std");

pub fn doSomething() std.os.wasi.errno_t {
    return std.os.wasi.fd_advise(0, 0, 2, std.os.wasi.advice_t.NORMAL);
}

main.zig:

const std = @import("std");

pub fn main() void {
    std.debug.print("native os: {}\n", .{@import("builtin").os.tag});
    std.debug.print("foreign os: {}\n", .{@import("foreign").os.tag});
    std.debug.print("Something naughty: {any}\n", .{@import("foreign").doSomething()});
}

build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "main",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    const foreign = b.addModule("foreign", .{
        .root_source_file = b.path("src/foreign.zig"),
        .target = b.resolveTargetQuery(.{ .os_tag = .wasi }),
    });
    exe.root_module.addImport("foreign", foreign);
    b.installArtifact(exe);
    b.step("run", "Run the executable").dependOn(&b.addRunArtifact(exe).step);
}

Error:

install
└─ install main
   └─ zig build-exe main Debug native 1 errors
/usr/local/bin/lib/std/os/wasi.zig:34:12: error: dependency on dynamic library 'wasi_snapshot_preview1' requires enabling Position Independent Code; fixed by '-lwasi_snapshot_preview1' or '-fPIC'
pub extern "wasi_snapshot_preview1" fn fd_advise(fd: fd_t, offset: filesize_t, len: filesize_t, advice: advice_t) errno_t;
           ^~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    doSomething: src/foreign.zig:5:23
    main: src/main.zig:6:71
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
  1. Correct, native is inferred if no target options are supplied to zig build. Using b.standardTargetOptions(.{}) lets users pass in desired targets via -Dtarget=... -Dcpu=...
1 Like

Thank you for providing an example with an actual compilation error!

I understand your point about builtin, but this means that code in the foreign module cannot observe its compilation target before runtime. For example, suppose the foreign module is target-agnostic except that it stores the OS tag in a comptime string for inclusion in bug reports. In this case, compilation would succeed, but the wrong OS tag would be stored. This is problematic.

To avoid problems like this, I think the build system should do one of the following, but I am unsure which:

  • ensure that any overrides of compilation options are reflected in builtin, or
  • refuse to import modules with incompatible compilation options.

I will wait a bit for further replies before opening a GitHub issue.

That’s a fair point, I’m also curious to see what others say. Because while I guess legal, compiling a module for a different OS than the executable that uses it seems… Funky. Like I’m trying to figure out what that use case would possible be.

See this GitHub issue: