# Comptime-known pointers of thread-local variables

**URL:** <https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890>\
**Category:** Brainstorming\
**Tags:** language\
**Created:** [July 24, 2026, 12:55am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890 "2026-07-24T00:55:15Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![chung-leong](https://ziggit.dev/user_avatar/ziggit.dev/chung-leong/32/999_2.png) [@chung-leong](https://ziggit.dev/u/chung-leong)\
**Post date:** [July 24, 2026, 12:55am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890/1 "2026-07-24T00:55:15Z")

</div>

Pointers to global variables are considered comptime-known in Zig. This fact allows us to synthesize multiple functions by binding one function to multiple variables. Here’s a simple example:

```zig
const std = @import("std");

const PFJMember = struct {
    name: []const u8,

    pub fn createGreetFn(comptime self: *@This()) fn () void {
        const ns = struct {
            fn greet() void {
                std.debug.print("Hello, {s}!\n", .{self.name});
            }
        };
        return ns.greet;
    }
};

var brian: PFJMember = .{ .name = "Brian" };
var loretta: PFJMember = .{ .name = "Loretta" };

pub const greetBrian = brian.createGreetFn();
pub const greetLoretta = loretta.createGreetFn();

pub fn main() void {
    greetBrian();
    greetLoretta();
}

```

This is a very useful technique. Everything falls apart though when a variable needs to be thread-local. The problem, of course, is the fact that we’re now dealing with multiple variables. A regular pointer can’t point to all of them simultaneously.

To get around this problem, I’m purposing the addition of a new builtin: `@tlsCast()`. The builtin accepts a pointer to a `threadlocal` variable and returns a pointer in the `.tls` address space (doesn’t exist yet). It’d yield the offset into a thread’s thread-local memory, basically.

To bind a threadlocal variable to a function we would do this:

```zig
threadlocal var reg: PFJMember = .{ .name = "Reg" };

pub const greetReg = @tlsCast(&reg).createGreetFn();

```

Since `@tlsCast()` returns a pointer of a different type, `createGreetFn()` in the example would need to accept `anytype` instead of `*@This()`:

```zig
    pub fn createGreetFn(comptime ptr: anytype) fn () void {
        const Self = @This();
        const ns = struct {
            fn greet() void {
                const self: *Self = switch (@typeInfo(@TypeOf(ptr))) {
                    .pointer => |pt| switch (pt.address_space) {
                        .generic => ptr,
                        .tls => @addrSpaceCast(ptr),
                        else => @compileError("Unexpected address space"),
                    },
                    else => @compileError("Pointer expected"),
                };
                std.debug.print("Hello, {s}!\n", .{self.name});
            }
        };
        return ns.greet;
    }

```

`@addrSpaceCast()` would basically add address of the current thread’s thread-safe storage to the comptime-known offset.

---

<div class="post-metadata">

**Author:** ![ScottRedig](https://ziggit.dev/user_avatar/ziggit.dev/scottredig/32/5880_2.png) [@ScottRedig](https://ziggit.dev/u/ScottRedig)\
**Post date:** [July 24, 2026, 1:27am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890/2 "2026-07-24T01:27:50Z")

</div>

If this effect is the only thing you need, this is already possible using a different technique:

```zig
const std = @import("std");

threadlocal var foo: u64 = 0;
threadlocal var bar: u64 = 0;

fn createCounterFunction(T: type, comptime name: []const u8) fn ([]const u8) void {
    return struct {
        fn counter(threadName: []const u8) void {
            @field(T, name) += 1;
            std.debug.print("On the {s} thread, {s} has been called {d} time(s).\n", .{ threadName, name, @field(T, name) });
        }
    }.counter;
}

const fooCount = createCounterFunction(@This(), "foo");
const barCount = createCounterFunction(@This(), "bar");

pub fn main(init: std.process.Init) !void {
    fooCount("main");
    barCount("main");
    var future = init.io.async(otherThread, .{});
    future.await(init.io);
    fooCount("main");
    barCount("main");
}

fn otherThread() void {
    fooCount("other");
    barCount("other");
}

```

Prints:

```console
On the main thread, foo has been called 1 time(s).
On the main thread, bar has been called 1 time(s).
On the other thread, foo has been called 1 time(s).
On the other thread, bar has been called 1 time(s).
On the main thread, foo has been called 2 time(s).
On the main thread, bar has been called 2 time(s).

```

---

<div class="post-metadata">

**Author:** ![ScottRedig](https://ziggit.dev/user_avatar/ziggit.dev/scottredig/32/5880_2.png) [@ScottRedig](https://ziggit.dev/u/ScottRedig)\
**Post date:** [July 24, 2026, 1:45am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890/3 "2026-07-24T01:45:46Z")

</div>

Oh, actually, I think I prefer this form more:

```zig
const std = @import("std");

const foo = Counter("foo");
const bar = Counter("bar");

fn Counter(name: []const u8) type {
    return struct {
        threadlocal var counter: u64 = 0;

        fn count(threadName: []const u8) void {
            counter += 1;
            std.debug.print("On the {s} thread, {s} has been called {d} time(s).\n", .{ threadName, name, counter });
        }
    };
}

pub fn main(init: std.process.Init) !void {
    foo.count("main");
    bar.count("main");
    var future = init.io.async(otherThread, .{});
    future.await(init.io);
    foo.count("main");
    bar.count("main");
}

fn otherThread() void {
    foo.count("other");
    bar.count("other");
}

```

Prints same output.

Note that the capture of name by the struct is important. If you’re not passing a value there, you’ll need to pass something unique. [See the 0.12 release notes.](https://ziglang.org/download/0.12.0/release-notes.html#Namespace-Type-Equivalence)

---

<div class="post-metadata">

**Author:** ![vulpesx](https://ziggit.dev/user_avatar/ziggit.dev/vulpesx/32/3989_2.png) [@vulpesx](https://ziggit.dev/u/vulpesx)\
**Post date:** [July 24, 2026, 3:18am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890/4 "2026-07-24T03:18:55Z")

</div>

> [@chung-leong](#):
>
> The problem, of course, is the fact that we’re now dealing with multiple variables. A regular pointer can’t point to all of them simultaneously.

There are two other fundamental issues with this.

Most importantly, they are `var`, and mutability cannot pass between runtime and comptime, in either direction, even if the pointer is `const`.

Allowing this for thread locals would be inconsistent with the rest of the language.

The second issue is that thread locals are allocated at runtime when spawning threads, which complicates lowering comptime known pointer to runtime, though it should still be possible.

---

<div class="post-metadata">

**Author:** ![leecannon](https://ziggit.dev/user_avatar/ziggit.dev/leecannon/32/82_2.png) [@leecannon](https://ziggit.dev/u/leecannon)\
**Post date:** [July 24, 2026, 9:28am UTC](https://ziggit.dev/t/comptime-known-pointers-of-thread-local-variables/16890/5 "2026-07-24T09:28:25Z")

</div>

Not only can’t you determine the pointer to a threadlocal until runtime, in the presence of dynamic linking you can’t know the offset into the tls of that threadlocal either.  
As a dynamically loaded library can have an unknown number of arbitrarily large threadlocals and it is up to to the runtime dynamic loader to combine them into a single tls.
