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
- A shared fn defined in a header file is made
local
to each translation unit that includes the header - when inlinin is enabled, the shared fn is inlined at every callsite in order to improve performance, thus givin the best of both worlds