Is it possible to assign a global constant at runtime?

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?

Any suggestions are appreciated, thanks!

const local_variable_declaration_text = "local_variable_declaration"; 
var local_variable_declaration_symbol: c.TSSymbol = 0;
fn init() void {

    local_variable_declaration_symbol = c.ts_language_symbol_for_name(tree_sitter_java(), local_variable_declaration_text, local_variable_declaration_text.len, true);
    std.debug.assert(local_variable_declaration_symbol != 0);
}
2 Likes

No, it is not possible.

You cannot use const to declare a mutable variable.
If you don’t use ‘pub’, the variables are private in each container (.zig file or struct).

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:

https://ziglang.org/learn/build-system/

7 Likes

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.

const LocalVariables = struct {
   a: c.TSSymbol,
   b: c.TSSymbol,
   c: c.TSSymbol,
};

pub const local_variables: *const LocalVariables = &local_variables_store;
var local_variables_store: LocalVariables = undefined;

fn init() void {
    inline for (@typeInfo(LocalVariables).Struct.fields) |field| {
        const symbol = c.ts_language_symbol_for_name(tree_sitter_java(), field.name, field.name.len, true);
        std.debug.assert(symbol != 0);
        @field(local_variables_store, field.name) = symbol;
    }
}
2 Likes

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:

const std = @import("std");
const stdout = std.io.getStdOut().writer();

const LocalVariables = struct {
   a: i32 = 0,
   b: i32 = 0,
   c: i32 = 0,
};

pub const local_variables = &local_variables_store;
var local_variables_store: LocalVariables = .{};

pub fn main() !void {
    // Zig allows to modify objects through const pointer
    local_variables.a = 11;
    local_variables.a = 42;
    try stdout.print("local_variables = {}\n", .{local_variables});
}

I am not aware of ways to “freeze” an object.

Explicitly naming the type allows you to specify whether the pointer is allowed to change the underlying memory with the const keyword:

pub const local_variables: *const LocalVariables = &local_variables_store;
3 Likes

Bingo! TIL :slight_smile: This solves it. I will make edit to my earlier post reflecting it.

1 Like

Just noticed that I left out the type. It’s what’s I was intending. Thanks for the correction.

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});
}
3 Likes

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.

2 Likes

Thanks for the suggestions. I went with @kristoff 's idea to add another build step which worked quite well.

1 Like