Static Inline in C


Today, I learnt of an interesting technique in C to share function definitions via a header file without causing conflicts

Lets take the example of a shared function that is defined in a header file

$ cat h.h
#ifndef _H_H
#define _H_H

int adderh(int a, int b) {
 return a * b + 2;
}
#endif

I use the function adderh in 2 different C files (also called translation units)

$ cat m.c
#include "h.h"

int multiplier(int a, int b) {
  return adderh(a, b) * 23;
}

~/personal ⌚ 1:39:37
$ cat n.c
#include "h.h"

int divider(int a, int b) {
  return adderh(a, b) / 12;
}

I then use the multiplier and the divider function from my main:

$ cat main.c
#include <unistd.h>
#include <stdio.h>

extern int multiplier(int a, int b);
extern int divider(int a, int b);
int main() {
  printf("%d\n", divider(multiplier(45, 33), 67));
  return 0;
}

The extern qualifier in main tells that these 2 fns are supplied by other object files or libraries that will be linked into the final executable. And indeed, I will link m.c and n.c into the final executable so:

$ gcc m.c n.c main.c -o main
duplicate symbol '_adderh' in:
    /private/var/folders/g5/6bsr90hd0xzg64415zgm1fxc0000gp/T/m-61ac34.o
    /private/var/folders/g5/6bsr90hd0xzg64415zgm1fxc0000gp/T/n-612007.o
ld: 1 duplicate symbols

Oh, what just happened ? Why is the linker complaining that there are 2 _adderh symbols ?

Header files are handled by the preprocessor. Think of them as rendering HTML templates. When you #include "somelibrary.h in your C file, the compiler will first expand the contents of the header INTO the .c file (a copy of it actually) before compiling and linking your code into the final excutable / object file. We can see how this happens by using the gcc with the -E flag

$ gcc -E m.c
int adderh(int a, int b) {
 return a * b + 2;
}

int multiplier(int a, int b) {
  return adderh(a, b) * 23;
}

So our m.c has 2 fns, one that is defined in the header expanded into our code and the original multiplier fn. Likewise the same happens for n.c as well.

Therefor when we link m.c and n.c into main, our compiler sees 2 adderh symbols which breaks the One Definition Rule that says that there must be only one definition for a given symbol (in our case, the adderh function)

We can try to solve this problem by making a function static. The static qualifier for a function makes it local to a translation unit. This means that the fn qualified with static is private to the translation unit and is not exported (in Javascript terms) to other translation units.

We can check this by compiling m.c to an object file and checking the list of exported or global symbols

$ cat h.h
#ifndef _H_H
#define _H_H

static int adderh(int a, int b) {
 return a * b + 2;
}
#endif
gcc -c m.c
nm -g m.o
$ nm -g m.o
0000000000000000 T _multiplier

The nm tool listed the symbols of an object file and -g lists only the global symbols. We can see that only the multiplier function is exported/global in our m.o

The same happens with n.c as well.

$ gcc -c n.c

~/personal ⌚ 2:00:39
$ nm -g n.o
0000000000000000 T _divider

Now that adderh is not exported by either object files, we can link them into our final executables without confusing our linker thus successfully creating our final executable

$ gcc m.c n.c main.c -o main

~/personal ⌚ 2:01:04
$ ./main
190955

It is also common in many codebases to find the static qualifier accompanied by an inline qualifier:

static int adderh(int a, int b) {
 return a * b + 2;
}
#endif

The inline qualifier is a hint to the compiler to inline the fn into the calling function. In certain cases such as when optimization flags are enabled, or when the compiler decides to optimize code, the call to the adderh in the multipler fn for example

int multiplier(int a, int b) {
  return adderh(a, b) * 23;
}

is replaced by the definition of the adderh fn itself

int multiplier(int a, int b) {
  return ( (a + b) * 2) * 23;
}

Eliminating fn calls can often lead to massive improvements in performance as the overhead of calling a fn can often be larger than the actual instructions carried out by the fn. 

To a sharp eyed observer, tt might look like `inline` itself should be sufficient to avoid our initial problem: that of having multiple adderh symbols because the adderh fn itself is erased from our object files. Shouldn't that work ? 

```shell
$ gcc m.c n.c main.c -o main
Undefined symbols for architecture arm64:
  "_adderh", referenced from:
      _multiplier in m-8a72fd.o
      _divider in n-34f18b.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Oh, no ! What happened ? Turns out the inline qualifier is actually just a hint to the compiler. The compiler may decide to inline the fn or it can decide not to. When I compile m.c to an object file with an inline adderh fn, the adderh symbol is marked as undefined, thus leavin to the linker to figure out how to find and link an adderh definition into the final executable.

$ gcc -c m.c
$ nm m.o
                 U _adderh
0000000000000000 T _multiplier
0000000000000000 t ltmp0
0000000000000038 s ltmp1

However if I enable optimizations with an -O2 flag, I can see that the adderh function is inlined into the multiplier fn

$ gcc -c -O2 m.c

~/personal ⌚ 2:20:13
$ nm m.o
0000000000000000 T _multiplier
0000000000000000 t ltmp0
0000000000000018 s ltmp1

Thus static inline ensures 2 things

  1. A shared fn defined in a header file is made local to each translation unit that includes the header
  2. when inlinin is enabled, the shared fn is inlined at every callsite in order to improve performance, thus givin the best of both worlds