Importing based on comptime value

Is it possible to import a library or a function based on a comptime value? Either through a build process or directly in main. I’m struggling to understand how can I use if else or switch statements during a comptime. Iv’e seen code where people use if builtin statements inside functions, but not for functions and imports themselves

Hey, welcome to Ziggit!

You can conditionally import something e.g. like this:

const foo = switch (my_condition) {
    .bar => @import("bar.zig"),
    .baz => @import("baz.zig"),
};

Note that @import is a bit of a special case and only takes string literals, not arbitrary comptime []const u8 vals like its signature would suggest. This makes sure that the compiler/tooling knows the entire import tree without having to resolve any comptime values first.

7 Likes

Are you looking for something like this?

pub const system = if(use_libc)
    std.c
else switch (native_os) {
    .linux => linux
    .plan9 => std.os.plan9,
    else => struct {
        pub const ucontext_t=void;
        pub const pid_t = void;
        pub const pollfd = void;
        pub const fd_t = void;
        pub const uid_t = void;
        pub const gid_t = void;
    },
}

That is from std.posix. The import isn’t being conditionally declared, but what is being used as the system is. Zig is lazily compiled, so all the imports that aren’t used won’t be compiled.

3 Likes

Yes, this is something I was looking for. In my case it would be this:

const builtin = @import("builtin");
const ctime = if (builtin.os.tag == .macos) @cImport({
    @cInclude("time.h");
});

But is there a similar way do to it for a function?

I’m not sure if I understand what you mean by that, can you elaborate please?

I think I understand now. I cannot use variables generated during comptime outside of it, and I should just branch my function body based on a builtin value. That way I can split the logic in a function. At first instance I wanted to split the logic for current time in a separate function and use it inside logFn but I understand that is not possible

fn logFn(
    comptime message_level: std.log.Level,
    comptime scope: @TypeOf(.enum_literal),
    comptime format: []const u8,
    args: anytype,
) void {
    if (builtin.os.tag == .macos) {
        // Using C library for datetime formatting
        const ctime = @cImport({
            @cInclude("time.h");
        });
        // Function to print out current time
        var time: [64]u8 = undefined;
        var now: ctime.time_t = ctime.time(null);
        const timeinfo = ctime.localtime(&now);
        const fmt = "%b %d %H:%M:%S"; // Example: "Oct 30 12:47:23"
        const time_len = ctime.strftime(time[0..].ptr, time.len, fmt, timeinfo);

        // Formatting time, log_level and scope as a single prefix
        const level_txt = comptime message_level.asText();
        var prefix_buf: [128]u8 = undefined;

        const prefix = std.fmt.bufPrint(
            &prefix_buf,
            "{s} [{s}]{s}",
            .{ time[0..time_len], level_txt, if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): " },
        ) catch "";
        if (@intFromEnum(message_level) <= @intFromEnum(log_level)) {
            std.debug.lockStdErr();
            defer std.debug.unlockStdErr();
            var stderr = std.fs.File.stderr().writer(&.{});
            nosuspend stderr.interface.print("{s}" ++ format ++ "\n", .{prefix} ++ args) catch return;
        }
    } else {
        if (@intFromEnum(message_level) <= @intFromEnum(log_level)) {
            std.log.defaultLog(message_level, scope, format, args);
        }
    }
}

1 Like

Ah sorry, yeah that’s certainly a good way to do it.

The other options would be to have multiple logFns with the same signature and choosing which one to use at comptime:

const logFn = switch (builtin.os.tag) {
    .macos => macosLogFn,
    else => defaultLogFn,
}

fn macosLogFn(
    comptime message_level: std.log.Level,
    comptime scope: @TypeOf(.enum_literal),
    comptime format: []const u8,
    args: anytype,
) void { ... }

fn defaultLogFn(
    comptime message_level: std.log.Level,
    comptime scope: @TypeOf(.enum_literal),
    comptime format: []const u8,
    args: anytype,
) void { ... }

but what you currently have is perfectly fine IMO

2 Likes

Agree with Justus2308, but want to point at that if you want to you can ‘anonymize’ the two functions like so

.macos => (struct { fn inner(…) void {…} }).inner,
1 Like

Thanks @Justus2308 . So, comptime switching is done with a variable calling a correct function. At least that is how I understand it. I don’t know the right terminology. Also anonymizing is nice as well. @affine-root-system Since we are on that topic now, is there a way to return anonymous structure as well from a function? I remember I’ve seen it but I was not able to figure out how to do it in fn declaration. What I mean by that is this:

fn name() !<this part here> {
  return  .{}
}

If you mean you want to declare a function with an automatically inferred return type based on the return statements in the function body (like in a language like TypeScript), it’s not possible. You always need to explicitly specify the return type (except for the error set part of an error union) so you essentially need to duplicate the main logic for the return type definition:

fn foo(comptime x: enum { a, b }) switch (x) {
    .a => i32,
    .b => []const u8,
} {
    return switch (x) {
        .a => 123,
        .b => "abc",
    };
}

If you mean declaring a struct type inline where it is used, that’s possible, just use struct {} where you otherwise would use an identifier:

fn foo() struct { a: struct { x: i32, y: i32 }, b: i32 } {
    return .{ .a = .{ .x = 1, .y = 2 }, .b = 3 };
}
2 Likes
fn name() !struct{int: i32, isbig: bool} {
  return  .{.int=1, .isbig=true};
}

Though I don’t think its that idiomatic. You will see tuples occasionally:

fn name() !struct{i32, bool} {
  return  .{1, true};
}
1 Like

FWIW, i believe you can just do const ctime = @cImport(@cInclude("ctime.h")); at top level and make sure to use it only on macos exclusive code (guarded by builtin.os). Zig lazy compile will not touch that variables unless used in branchs

const std = @import("std");
const builtin = @import("builtin");
const ctime = @cImport(@cInclude("time.h"));

pub fn main() void {
    std.log.info("hello world", .{});
}

fn formatCurrentTime(_: void, w: *std.Io.Writer) !void {
    const time = try w.writableSliceGreedy(64);
    var now: ctime.time_t = ctime.time(null);
    const timeinfo = ctime.localtime(&now);
    const fmt = "%b %d %H:%M:%S"; // Example: "Oct 30 12:47:23"
    const time_len = ctime.strftime(time.ptr, time.len, fmt, timeinfo);
    w.advance(time_len);
}

fn logFn(
    comptime message_level: std.log.Level,
    comptime scope: @TypeOf(.enum_literal),
    comptime format: []const u8,
    args: anytype,
) void {
    if (builtin.os.tag == .macos) {
        std.debug.lockStdErr();
        defer std.debug.unlockStdErr();

        var buf: [64]u8 = undefined;
        var stderr = std.fs.File.stderr().writer(&buf);
        const w = &stderr.interface;

        w.print("{f} [{t}]", .{ std.fmt.Alt(void, formatCurrentTime){ .data = {} }, message_level }) catch return;
        if (scope == .default) {
            w.writeAll(": ") catch return;
        } else {
            w.print("({t}): ", .{scope}) catch return;
        }
        w.print(format, args) catch return;
        w.writeAll("\n") catch return;
        w.flush() catch return;
    } else {
        std.log.defaultLog(message_level, scope, format, args);
    }
}

pub const std_options: std.Options = .{
    .log_level = .info,
    .logFn = logFn,
};
2 Likes

Yes, I meant automatically inferring a return type from a return statement. Good to know, thank you.

Yes, that was my initial approach. So, even if I write functions and variables, with out specifying a comptime value, zig will not compile them if not used? If that is the case then I don’t have to worry about the bloat in a binary for other systems

Yup. As long as it is not referenced, it will not be in the final library. You can check by just changing the header to a bogus value, on non-MacOS it will still compile.

Same apply to scope. As you see, my code contains a compile error at w.print("({t}): ", .{scope}) because we cannot print .enum_literal with t (yet?). Zig just ignores that branch because there is no log.scoped in this whole Zig program.

zig std is also full of stuff specific to OSes, like functions for Windows in the same file. Those would not compile on other platforms, let alone being in the final library :smiley:

(And if you did not notice, I also removed the log level checking logic because std_options.log_level already do that for you. It will even skip compiling your whole logFn.)

1 Like

Yes, you also changed formatTime function. I was returning a slice of a buffer initially and indeed, There is no message_level check. I was just following what was written in a docu for a custom logFn but that was not working when I was outputting to a file on MacOS. Need to test it on Linux still. Text was not fully written to a file, but only parts of it. I guess because it was missing flushing. I added a flush after a print, but it still was not printing all output to a file. std.debug instead of std.fs.File was printing fine. No matter if I was using File.stdout() or stderr

stdio are unbuffred by default in Zig, so they go straight out without flushing, but this means they are slower.

I was looking at the code and I’ve seen that std.debug is using streaming. Iv’e tested these:

std.debug.lockStderrWriter(&buf);

std.fs.File.stderr().writer(&buf);
std.fs.File.stdout().writer(&buf);

std.debug. when piped to a file with app 2>file.log works perfectly fine, all is visible and if I try any of the other writers, they are segmented. Everything else is the same in a function. Same behavior on MacOS and Linux. In terminal, all output that was displayed was correct. Only issue was when sending an output to a file.

dont use the same buffer for multiple readers/writers at the same time, they have no knowledge of whatever else is interacting in the buffer so they will corrupt each other’s data.

1 Like

That is not the issue. I’m using single logging function to output the logs. Tried with my old and iceghost’s function above:

fn logFn(
    comptime message_level: std.log.Level,
    comptime scope: @TypeOf(.enum_literal),
    comptime format: []const u8,
    args: anytype,
) void {
    if (builtin.os.tag == .macos) {
        std.debug.lockStdErr();
        defer std.debug.unlockStdErr();

        var buf: [64]u8 = undefined;
        var stderr = std.fs.File.stderr().writer(&buf);
        const w = &stderr.interface;

        w.print("{f} [{t}]", .{ std.fmt.Alt(void, formatCurrentTime){ .data = {} }, message_level }) catch return;
        if (scope == .default) {
            w.writeAll(": ") catch return;
        } else {
            w.print("({t}): ", .{scope}) catch return;
        }
        w.print(format, args) catch return;
        w.writeAll("\n") catch return;
        w.flush() catch return;
    } else {
        std.log.defaultLog(message_level, scope, format, args);
    }
}

Only thing I changed was this part. from:

        std.debug.lockStdErr();
        defer std.debug.unlockStdErr();

        var stderr = std.fs.File.stderr().writer(&buf); // Or std.fs.File.stdout().writer(&buf);
        const w = &stderr.interface;

To:

        const w = std.debug.lockStderrWriter(&buf);
        defer std.debug.unlockStderrWriter();

And 2>file.log was outputting everything. I tested it in this code:

const ifaddrs = extern struct {
    ifa_next: ?*ifaddrs,
    ifa_name: [*c]const u8,
    ifa_flags: c_uint,
    ifa_addr: ?*std.posix.sockaddr,
    ifa_netmask: ?*std.posix.sockaddr,
    ifa_dstaddr: ?*std.posix.sockaddr,
    ifa_data: ?*anyopaque,
};

extern fn getifaddrs(ifa: *?*ifaddrs) c_int;
extern fn freeifaddrs(ifa: ?*ifaddrs) void;

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    // var buf: [64]u8 = undefined;
    _ = allocator;
    var ifap: ?*ifaddrs = null;
    if (getifaddrs(&ifap) != 0) return error.GetIfAddrsFailed;
    defer freeifaddrs(ifap);

    var cursor = ifap;

    while (cursor) |ifa| {
        if (ifa.*.ifa_addr) |sa| {
            const family = sa.family;
            if (family == std.posix.AF.INET) {
                const tmp_sa: *align(4) std.posix.sockaddr = @alignCast(sa);
                tmp_sa.* = sa.*;
                const name = std.mem.span(ifa.*.ifa_name);
                const addr = std.net.Address.initPosix(tmp_sa);
                std.log.debug("{s}: {f}\n", .{ name, addr });
            }
        }
        cursor = ifa.*.ifa_next;
    }
}

By bad output I mean this:

 ~/GIT/zig-playgorund % zig-out/bin/zig_playgorund 2>file.log && cat file.log 
Nov 01 08:49:25 [debug]: utun4: 10.254.0.2:0

2:0

By good output I mean this:

 ~/GIT/zig-playgorund % zig-out/bin/zig_playgorund 2>file.log && cat file.log
Nov 01 08:53:08 [debug]: lo0: 127.0.0.1:0

Nov 01 08:53:08 [debug]: bridge100: 10.211.55.2:0

Nov 01 08:53:08 [debug]: bridge101: 10.37.129.2:0

Nov 01 08:53:08 [debug]: en0: 192.168.1.8:0

Nov 01 08:53:08 [debug]: utun4: 10.254.0.2:0