Blog by Railsware

Global variables in Swift are not variables

Global variables in Swift are not variables

The title sounds pretty strange, but I will explain what it means.

While playing with Swift I’ve faced weird behaviour, a feature in fact, which isn’t covered in documentation (at least I didn’t find any references!).

UPD: shame on me, how could I miss this?

Global constants and variables are always computed lazily, in a similar manner to Lazy Stored Properties. Unlike lazy stored properties, global constants and variables do not need to be marked with the @lazy attribute.

Global variables are functions and their actual values instantiate on demand

The problem

I wanted to perform some function from different translation unit without explicit calling, and decided to try old C++ trick with global variable:

#include <iostream>

int globalFunc() 
{
    std::cout << "hello, world!\n";
    return 42;
}

int globalVar = globalFunc();
int main(int argc, const char * argv[])
{
    return 0;
}

After compiling and running this program you’ll see “hello, world!” in the output, so I’ve tried the same trick with swift:

import Foundation

func globalFunc() -> Int 
{
    println("hello, world!")
    return 42
}

var globalVar = globalFunc()
// currently, this file is empty

But had no luck… This program does nothing in swift.

Explanation

At the first glance, it looks like compiler optimisation, but what if we call this variable from debugger?

(lldb) po globalVar
hello, world!
42

(lldb)

It works. Weird. But it works!

Ok, so let’s access to this variable from main.swift:

var localOne = globalVar

It works as well, so variable instantiates at the first call. But how it works and why?
To understand how it works we definitely should dive deeper into the executable. Usually I’m using LLVM IR for such purposes, but this time I decided to use Hopper, ’cause it has a very useful feature: ‘Show pseudocode’

After opening the executable via Hopper you may see that app also has function main as an entry point, this function calls _top_level_code, which consists of main.swift. Let’s look at pseudocode of _top_level_code:

function _top_level_code {
    rax = __TF10swift_testa9globalVarSi();
    rax = *rax;
    *__Tv10swift_test8localOneSi = rax;
    return rax;
}

and get rid of ‘garbage’

v_localOne = F_globalVar();

Instead of direct access to variable, it calls respective function; let’s look at it:

function __TF10swift_testa9globalVarSi {
    swift_once(_globalinit_token0, _globalinit_func0, 0x0);
    return __Tv10swift_test9globalVarSi;
}

it calls another function swift_once and returns actual variable globalVar. Ok, let’s go deeper, currently we’re interested in the second parameter: _globalinit_func0, it’s also function:

function _globalinit_func0 {
    rax = __TF10swift_test10globalFuncFT_Si();
    *__Tv10swift_test9globalVarSi = rax;
    return rax;
}

Caught it! Finally, we’ve found function which does exactly what we want:

v_globalVar = F_globalFunc()

Conclusion

Any global variable in Swift is being replaced with respective function call that initializes this variable on demand

Let’s wrap up our small research!

This code

func globalFunc() -> Int
{
    println("hello, world")
    return 42
}

var globalVar = globalFunc()
var localOne = globalVar

translates into this one:

func F_globalFunc() -> Int
{
    println("hello, world")
    return 42
}

var v_globalVar_init = 0
var v_globalVar = zero

func F_globalVar_init()
{
    v_globalVar_init = 1
    v_globalVar = F_globalFunc()
}

func F_globalVar() 
{
    if (v_globalVar_init == 0) {
        F_globalVar_init()
    }
    return v_globalVar
}

// ....
var v_localOne = F_globalVar()

So, now this behaviour is clear, but question ‘why such a decision was made?’ is still open.

Happy hacking, guys, and be careful!

Exit mobile version