In my personal project I’m linking against a c library, treesitter, and I have these global variables which I’m defining at runtime through an init function. I’d like these variables to be const, but the value isn’t known until runtime since I’m calling a function from a library. Is there a better way to do this?
It would be even better if I could somehow have these values known at comptime so I can use them in switch statements. Is it possible to call a c library during comptime?
The correct way of implementing this would be via the build system.
Have one step that runs a Zig program that collects all those symbol IDs into some kind of simple data structure, then expose it to your main program as a module that can be imported either via @import or @embedFile.
Each of the things that I mentioned here are showcased as an example here:
Storing these variables in a struct field and then making the struct available through a const pointer seems most reasonable solution here. That’d also allow you to initialize them using an inline loop.
Unfortunately, this is not a solution to constant variables initialized at runtime. Zig allows to modify object behind a const pointer so that the fields are mutable. Here is an example with TreeSitter types replaces by i32 for simplicity:
I think, I found the most practical solution to a set of constants initialized once at runtime. Thanks to @IntegratedQuantum for explaining some aspects of const pointers. I implemented ConstSingleton generic pattern to make my solution more general.
const std = @import("std");
const stdout = std.io.getStdOut().writer();
/// `ConstSingleton` defines a const singleton object that wraps `struct T` containing
/// `fn init(self: *T, args: anytype) void` that initializes T object.
/// Calling init function at runtime more than once results in error.
pub fn ConstSingleton(comptime T: type) type {
return struct {
const Self = @This();
var inner: T = undefined;
var initialized: bool = false;
/// get const pointer to the inner object
pub fn get() *const T { return &inner; }
/// Call init function defined in T passing it `.{args...}`
pub fn init(args: anytype) !void {
if (initialized) {
std.log.err("ConstSingleton: repeated initialization", .{});
return error.RuntimeError;
}
@call(.auto, T.init, .{&inner, args});
initialized = true;
}
};
}
pub const LocalVariables = ConstSingleton(struct {
a: i32,
b: i32,
c: i32,
pub fn init(self: *@This(), args: anytype) void {
self.a = args.a;
self.b = args.b;
self.c = args.c;
}
});
pub const local_variables = LocalVariables.get();
pub fn main() !void {
try stdout.print("Const Singleton with one-time initialization\n", .{});
try stdout.print("--------------------------------------------\n", .{});
try LocalVariables.init(.{.a=1,.b=2,.c=3});
// try LocalVariables.init(.{.a=4,.b=5,.c=6}); // ERROR: repeated initialization
// local_variables.a = 11; // ERROR: cannot assign to constant
try stdout.print("local_variables = {}\n", .{local_variables});
}
An alternative might be to use memory mapping, if you have access to it, (I think wasm doesn’t have support for it?) map a page, initialize it to the wanted memory and then set it to read only.
In this case it might even make sense to just generate a special file that contains all the data once, whenever it is convenient to generate it and then just map it. But if it rarely changes using a build step makes sense, that way it just gets compiled into the read only section of the executable.