Global variables vs Static variables

Hello everyone, can someone explain the difference between global and static variables in Zig? Also, is it possible to define a global variable without storing it in static memory? If not, why do we not have control over defining variables in static memory (just like in C)? And why are const always stored in static memory?

thank you

Hello @esn Welcome to ziggit :slight_smile:

There is no difference between global and static variables. It is memory storage, at a specific address, allocated for the variable.

No, because global and static variables are allocated by the linker in the same storage space.

Zig have static variables, but they are declared different from C.


When a struct is defined, the struct can contain fields and declarations.

const S = struct {
    instance_value: i32, // instance_value is a field
    var static_value: i32 = 0, // static_value is a declaration
}

Each instance of the struct S have its own instance_value, because it is a field.
All the instances of the struct S share the same static_value, because it is a var declaration.

See also the zig language reference on Static Local Variables

7 Likes

In addition to @dimdin’s answer, I think it’s important to understand that global is a bit of a misnomer in Zig. When you’re writing code in a .zig file, you are writing code in a struct. It’s not like C++ where you have structs and then you have the great outdoors beyond them. Think of it this way:

// top of main.zig
const Self = @This();

pub fn main() !void {

    // this will not hit the compile error...
    // proving that "This()" is a struct.
    if (@typeInfo(Self) != .Struct)
        @compileError("ouch");

    // more stuff...
}

All that said, @esn I encourage you to play around with the build system and think about it in the way that I’ve presented here.

4 Likes

@dimdin @AndrewCodeDev
Thank you very much for your explanations, I now have a better understanding of this.

When you’re writing code in a .zig file, you are writing code in a struct. It’s not like C++ where you have structs and then you have the great outdoors beyond them.

Does this approach have any advantages?

A couple - imports are just structs and you can analyze them using meta-programming (which is really just reflection over types in this case).

I can check to see if an import has a publicly declared function in the same way I can for any other struct in my program. I can pass them around as arguments, check for member variables, check the size of the type… etc…

In this sense, imports aren’t something semantically special compared to other objects you’ll use while programming.

5 Likes

static has a bunch of different meanings in C/C++.

zig is no different than C or C++ is far as that is concerning, you just define and reference them a little different.

vars (in all c++/c/zig) all have two properties: lifetime and scope. Lifetmie is how long the variables exists and scope is where it can be accessed from. They are pretty related most of the time.

global usually refers to scope in that it can be accessed from anywhere. static refers to lifetime covering the entire program. so all globals are static, but think of a static variable in a function in c/c++. It can only be accessed while in the function (function scoped), but between invocations it still is there and doesn’t go away. top level variables in c/c++ are global but since C and c++ have obj and so (dll) files the link/loader has to get involved and get into this whole complex subject of symbol exports so let’s just dodge that and not speak of them ever again. (zig only has one translation unit so there isn’t really the same thing in zig).

Here’s the C/C++ and Zig equivs.

c++ class statics and zi struct statics are both per type and not per instance, to basically the sae:

class Cls {
public:
  int instance_var;

  static int x = 0;
  void inc() { x += 1; }
  // or  void inc() { Cls.x += 1; }
   // or void inc() { this->x += 1; }

};

const Cls = struct {
  instance_var: u32,

  var x: u32 = 0;
  pub fn(this: @This()) void { x += 1; }
  // or pub fn(this: @This()) void { Cls.x += 1; }
  // NOT this.x 
};

function scope static need to be wrapped in a stuct – every static needs ot be wrapped in a struct. Structs in Zig are also the namespace feature, so you make a struct with a static - the same as above.

void cfunc() {
  static int x = 0;
  x =+ 1;
}

fn zfunc() void {
    var s: struct { var x: u32 = 0 };
    s.x += 1;
}

that brings us to file scoped. Since a file is a namespace, that means its actually a struct too. you can imagine the entire file being wrapped in a struct named the same as the file minus the extension. So it looks like a struct static.

static int x = 0;

// prentend the entire file is wrapped in a struct { ... }
var x: u32 = 0;

C and C++ have some added complexity with static basically being a linker instruction (inline in c++ is the same way - it mean emit weak symbol and no longer affects inlining - fun huhh).

4 Likes

Enormous advantages, yes.

C has a concept of a ‘translation unit’, which is essentially a C file once the preprocessor is done with it. This comes along with ‘internal linkage’, an implicit scope, and so on. Header files are necessary to inject types into a translation unit which doesn’t contain them. All of this is eventually resolved by the linker.

All of this has to be handled by the programmer, and most importantly, the C language itself has no concept of these things, no way to reflect on them or manipulate them.

In Zig, a file is a container type, specifically an anonymous struct. This is much nicer to work with in every conceivable way. In C, if you want to know where some external function or type comes from, you read every single #include until you find it. If you mis-spell something, the compiler can’t help you find the object code you were actually looking for. Zig has exactly none of these problems.

In addition to user ergonomics, this offers considerable opportunity for whole-program optimization, in a way which is more difficult (read: time-consuming) in C.

As another example, let’s say you want to combine all your files into one file for distribution (this is called an amalgamation, SQLite is one library which ships this way). You really must design your entire codebase to support this, because when those files become one file, they become one scope, so you have to plan in advance to have no name collisions.

In Zig it’s almost trivial: make each file into a struct, and give that struct the name which the other files expect it to have. Done. Decide that a directory should be one file instead of several? Same deal. Just make some minor changes to consumers and you’re done.

4 Likes