Calling extern functions at comptime

Is there any way to call extern functions at compile time? When I try it the normal way using the comptime keyword it produces an error.

For my use case I need to get the timestamp of when the program was compiled so I could use it later on but the only way to do that is to call timestamp with comptime.

My error:

zig-windows-x86_64-0.13.0\lib\std\time.zig:100:53: error: comptime call of extern function
            windows.kernel32.GetSystemTimeAsFileTime(&ft);
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
zig-windows-x86_64-0.13.0\lib\std\time.zig:84:53: note: called from here
    return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_us)));
                                       ~~~~~~~~~~~~~^~
src\example.zig:32:55: note: called from here
    const timestamp = comptime std.time.microTimestamp();
                               ~~~~~~~~~~~~~~~~~~~~~~~^~

This is not possible. Extern functions are either resolved dynamically during runtime (which is certainly the case for these operating system functions) or they are linked statically by the linker, which happens after compilation is already finished.

You can however call extern functions in the build.zig and pass the result to the code.

3 Likes

Oh, thank you. I didn’t realize you could do things like that in build.zig.

I have another question, is it possible to have a comptime state similar to runtime?

For example when you call a function multiple times in comptime it could produce non-deterministic outputs or would I need to include some sort of state struct?

No, comptime state is not possible. From what I gathered it would make incremental compilation more difficult since each comptime function call could now have side effects.
Depending on what you are trying to achieve, there might be alternatives, either using the build system, or other measures.

Hi welcome to the forum. This can be solved by leveraging the build system I’m not sure what’s the most idiomatic way of doing it, but for example you could use

    const build_timestamp = std.time.timestamp();
    const option = b.addOptions();
    option.addOption(@TypeOf(build_timestamp), "build_timestamp", build_timestamp);

    const exe = b.addExecutable(.{
        .name = "t",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.root_module.addOptions("timestamp", option);
    b.installArtifact(exe);

and then in your main or wherever you need it.

const std = @import("std");
const meta = @import("timestamp");

pub fn main() !void {
    std.debug.print("{d}", .{meta.build_timestamp});
}

I’m not sure how you could improve the precision, or whether it matters to you, but at least this gives you a close enough value that’s comptime available.

Of course in the build system you could even go further, and convert the time stamp to local time and pass a string to your program or whatever is it that you need.

I think you are right about global mutable state.

I am not completely sure what we consider comptime state.
I think using comptime vars, you at least get locally scoped comptime state, for example with zig 0.13 I can do this:

const std = @import("std");
const builtin = @import("builtin");

const ComptimePrng = struct {
    const Prng = std.rand.DefaultPrng;
    rng: Prng,

    pub fn comptimeInit() ComptimePrng {
        comptime {
            return .{
                .rng = Prng.init(0), // imagine a different seed being passed from build system via build option
            };
        }
    }

    pub fn getRandomColor(self: *ComptimePrng) [3]u8 {
        var color: [3]u8 = undefined;
        self.rng.random().bytes(&color);
        // copy to const is not strictly needed here,
        // but it is a useful pattern as soon as you have pointers to other things
        // and later want to use these at runtime.
        // The pointers that get exposed to the runtime need to be const pointers
        // that only contain other const pointers.
        //
        // However you can build quite interesting things with comptime vars
        // accumulating things, then once things are built,
        // you can create a deep copy with all const pointers.
        //
        // This requires that there aren't cycles because we still don't have
        // a way to create cyclic data structures with const pointers,
        // that doesn't use some mutation trick.
        const res = color;
        return res;
    }
};

pub fn main() !void {
    comptime var builder = ComptimePrng.comptimeInit();

    const colors = comptime blk: {
        var c: [10][3]u8 = undefined;
        for (&c) |*d| d.* = builder.getRandomColor();
        const res = c;
        break :blk res;
    };

    std.debug.print("-----\n", .{});
    for (colors) |c| std.debug.print("color {any}\n", .{c});
    std.debug.print("-----\n", .{});

    const more_colors = comptime blk: {
        var c: [5][3]u8 = undefined;
        for (&c) |*d| d.* = builder.getRandomColor();
        const res = c;
        break :blk res;
    };

    std.debug.print("-----\n", .{});
    for (more_colors) |c| std.debug.print("color {any}\n", .{c});
    std.debug.print("-----\n", .{});
}

This produces deterministic results, but like @pierrelgol has already shown it would be easy to change the seed value of the Pseudo Random Number Generator via a build option, to get results that look random, but aren’t.

-----
color { 223, 35, 11 }
color { 7, 213, 128 }
color { 252, 123, 154 }
color { 26, 94, 190 }
color { 234, 94, 74 }
color { 154, 141, 240 }
color { 110, 2, 181 }
color { 89, 201, 75 }
color { 255, 240, 137 }
color { 22, 76, 66 }
-----
-----
color { 104, 153, 193 }
color { 252, 77, 170 }
color { 193, 12, 150 }
color { 32, 231, 250 }
color { 250, 16, 172 }
-----

Personally I think that Pseudo Random Number are great because they give you most of what is needed from random numbers, but also allow you to specify the seed to get reproducible and thus debug-able code.

Classical computers don’t produce true non-determinism by design (at least not directly), if it appears it is either because some kind of non-deterministic signal like from some sensor got introduced, or because your program runs multiple threads and is basically observing the non-determinism that gets created by the OS doing its own complicated scheduling things (which either may be pseudo random or are influenced by external signals which are considered sources of actual randomness).

But in general build options / the build system is a good way to add things into your program, that aren’t directly accessible from comptime.

I think this locally scoped mutable state is limited and good, because it still can be understood easily and it doesn’t allow you to create functions that just magically return different results without an explanation. But it still allows limited forms of things that have side effects, they are just contained in a scope and still deterministic.

1 Like