Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It looks like in zig all functions are `constexpr` ?

As for `constinit`, it looks like ziglang doesn't have an answer for the problem that constinit solves. That is, ziglang appears to have the same bugs here that C++ did before constinit was added.



Can you give an example of a bug in a Zig program caused by the lack of constinit? It seems like Zig just does not have the delayed initialization of static and thread-local variables that C++ has, but I'm not completely sure of this.


Unless I'm misreading this: https://ziglang.org/documentation/master/#Container-Level-Va...

zig has lazily initialized at runtime mutable globals, and compile-time initialized constant globals (so C++'s constexpr variables). But what it doesn't have is compile-time initialized mutable globals (which is what `constinit` does)


    // compile-time initialized mutable global:
    var foo = bar();


> If a container level variable is const then its value is comptime-known, otherwise it is runtime-known.

I'm not sure how to reconcile that Zig documentation with your comment's claim. It appears that Zig relies on opportunistic behaviors here instead of contracts. That is, your var foo could be comptime known, but isn't required to be. Which is fine enough, but not comparable to `constinit` either.

Alternatively if all container variables are exclusively comptime initialized, then the obvious missing part from Zig would be the inverse of constinit which is the onload initializer behavior.


The quoted documentation says if you initialize a mutable variable to 5 at compile time and later change it to 2 and print it, it won't print 5, because uses of that variable in expressions won't be resolved to the constant value used to initialize it at compile time.

When is onload? I don't think that's a time that occurs during program execution in Zig.


Does Zig not have an equivalent of `__attribute__((constructor))`?

If not then that'd be the 3rd type of initialization it's missing, the equivalent of C++'s nothing specified.


It's relatively easy to solve the problem in a language with acyclic imports by doing a topological sort of the imports, and initializing in that order. In C++, with textual includes and no standardized control over the linker, you don't have the information or well-defined control to order the initialization, so you have a problem.


If it was "relatively easy" then there wouldn't be a million dependency injection solutions for Java of ungodly runtime cost to try and solve this problem.

People want to initialize stuff at load library time. They will always want to do this. If the language doesn't provide a solution, people will just bolt on shitty add-on toolchains to do it anyway.

This isn't a uniquely C++ problem (__attribute__((constructor)) is after all for C, not C++).

Take for example Rust, which also doesn't provide an attribute((constructor)) equivalent and says nothing happens before or after main. Except https://crates.io/crates/ctor exists to immediately say "fuck that noise" and racked up >20m downloads.


> If it was "relatively easy" then there wouldn't be a million dependency injection solutions for Java of ungodly runtime cost to try and solve this problem.

You realize Java has the feature you're discussing, without dependency injection?

   class Foo {
      static {
           initialize();
      }
    }

They even have a well defined execution order:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.htm..., section 8.7, with order defined in section 12.4


That runs at class load time, not at library/executable load time as c++ static constructors or __attribute__((constructor)) does.

So no, that's not equivalent. Not even close. This is why you see things in Java that scan through loaded jars looking for all classes with attributes to just speculatively load. To recreate what c++ has natively & C has with a broadly supported extension.


Okay but we're talking about native languages where you are bound by the rules of your platform's dlopen / LoadLibrary implementation ; this is where so many static initialisation bugs lurk, when you load libraries dynamically


No, you're not. The fact that a misfeature exists in the toolchain doesn't force you to use it.

You can order the init sections on your own when you generate the binaries and add your own constraints, and if your toolchain doesn't give you that control, you can generate and call your own init functions. (Since I didn't want to dick with the linker too much, I generated my own init functions in my natively compiled language to solve this problem).

Your language's dlopen wrapper can also dlsym the initializer and call it if it's present.

This is very much a C++ problem, because its separate compilation model throws away initializer ordering, and prevents the compiler from doing anything about it. And, of course, compatibility makes this a tough sell for C++ -- languages unconstrained by history don't have to worry about this.

tl;dr: Ignore the constructor attribute/init section, and generate your own function that orders things correctly. Then call it before main.


> tl;dr: Ignore the constructor attribute/init section, and generate your own function that orders things correctly. Then call it before main.

How does my shared library do that?


     def loadlib() {
        lib = dlopen("libfoo.so")
        sym = dlsym(lib, "pkgname.__init__")
        sym()
        return lib
     }
Hell, you can even do this with a single constructor attribute that runs the initializer, if you want to be fully dlsym compatible.

If you have a DAG of dependencies, it's impossible for your library to care whether main or dlopen has called its initializer, because the same initialization order of dependencies is observed in either case. The only thing you need to beware of is guarding against double-init if you dlopen, and the generated library-level initializer code can do that.


So the answer is you can't, and you're just too stubborn to say as much. Not all languages need all features, zig is allowed to just be insufficient for things, this being one of them.


???

If you generate a single initialization symbol that guards against multiple runs in the init section, this works just fine with dlopen.

This isn't even hard, but you need to be able to form a full dag of imports.

The only problem is that the linker slams together the .init, .ctors, or .init_array sections in some arbitrary order (usually the order that .o files are passed in on the command line, but there are no guarantees). If you topologically sorted the object and library list by import order, things would work out. But because the linker doesn't cooperate, a language that wants to avoid the initialization order fiasco needs to provide its own ordering. Or its own linker.

As far as Zig goes -- I have no clue what it does. Never touched it.

Also, as a side note -- initializers were initially designed so that there would be exactly one init function per binary or library, and then people wanted to split the initialization up, so the way the init section ends up getting constructed on most unixes is fascinatingly hacky: The linker links `crti.o`, your .o files, `crtn.o`. crti.o contains a function prologue; the init sections in your .o files contain a sequence of call functions, and crtn.o contains a function epilogue. Stitch them all together in order, and you get a single function that you can call.

This is deprecated, and now there's a table of function pointers (.init_array).


Libraries don't exist in a vacuum with a single well defined user. If I'm shipping, say, libpng.so, and I need to do a one-time initialize per process, the only feasible way to do that is with a c++ static constructor or __attribute__((constructor)). It's why those things exist. It's why linker .ctors exist. It's why rust-ctor crate exists. It's why Java land re-created that by self-scanning all open jars for classes to randomly load.

This is a very widely used capability and it is very definitely not trivial to just make an init function & define the import dag and have that work reliably. People will forget to call it & most of the time they won't have any clue what their dependency DAG even is. And that's assuming it can even form a DAG at all - cyclic dependencies are absolutely a thing, after all.


Libraries exist with a well defined compiler. The solution is at the compiler level, using information coming from the import graph that exists in languages with a module system.

And I'm not sure how you can forget to import a library and still expect to use it, assuming your language has a module system.

C and C++ can't solve this problem, of course, due to textual inclusion.


> The solution is at the compiler level, using information coming from the import graph that exists in languages with a module system.

no, it isn't because your library has to integrate with other languages when you make native stuff. I should be able to call dlopen from any native language without having to care in which language your library was implemented, just calling my OS's dlopen / LoadLibrary OS API function should be enough. If this doesn't work, then your solution is just not acceptable in many cases, so yes you have to work with its limits.


Can you explain how a compiler topologically sorting the entries in .init_array makes a library unusable? What breaks?


How does one assert bar() or better yet, an arbitrary expression you place there, is pure?


Well, the compiler attempts to evaluate bar(), and if it cannot, you get a compile error




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: