Static and dynamic linking
This is a sloppy personal note on the basic of static and dynamic libraries in C programming.
Compile directly from source
Suppose I have a simple program at hello.c
/* hello.c */
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("你好,🌍!\n");
return 0;
}
The OS-native C compiler can compile it straight into an executable:
cc hello.c -o hello
# Should print the message
./hello
Sometimes functionalities need to be split into two source files. Consider the following example with two files:
libhello.cwhich contains the functionvoid *hello(const char *name)hello.c, which is the main program.
/* libhello.c */
#include <stdio.h>
void greet(const char *name) {
printf("你好,%s!\n", name);
}
For hello.c to use greet, some declaration is required.
One way is to use the extern keyword.
#include <stdio.h>
extern void greet(const char *name);
int main(int argc, char *argv[]) {
greet("🌍");
return 0;
}
If there is no declaration, the compiler will complain:
error: call to undeclared function ‘greet’
If there is a declaration but no implementation, then the linker will complain:
Undefined symbols for architecture arm64:
"_greet", referenced from:
_main in hello-45e4f0.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Note that it is okay to declare a function without implementation if the function is never called.
Typically, the library developers are a different group from the library users. Developers can use a header file to “advertise” the public API of the library, so the user does not have to declare the functions manually.
A header file looks like this:
/* libhello.h */
#ifndef LIBHELLO_H
#define LIBHELLO_H
void greet(const char *name);
#endif /* LIBHELLO_H */
The compiler needs to be aware of where the header file is located.
In this example, libhello.h is located at include/libhello/libhello.h,
so we need to add the include/ directory to the compiler command:
cc -I$(pwd)/include src/libhello.c src/hello.c
With the -I/path/to/include flag, hello.c can use libhello.h using an include statement:
#include "libhello/libhello.h"
Clangd can be configured to discover header via .clangd:
CompileFlags:
Add: ["-I/path/to/your/include"]
Static linking
The library developer can compile the library code into a static library (.a on UNIX systems),
then provide the user with the libhello.h header and the libhello.a library file.
# Use -c flag to compile libhello.c into object file
cc -c src/libhello.c -o build/libhello.o
# Insert the object file (r flag) into archive file, create if not exist (c flag),
# create index for faster linking (s flag)
ar rcs build/libhello.a build/libhello.o
# Compile binary using the static library instead of the source code
cc -I$(pwd)/include src/hello.c build/libhello.a -o build/hello
Check the size of the binary ls -lh build/hello. Mine shows 33464 bytes.
Time the execution of the binary with time build/hello:
build/hello 0.00s user 0.01s system 2% cpu 0.329 total
Dynamic linking
With dynamic linking, the function greet will be loaded at program runtime instead of compile time.
First, compile the library code into a shared library with the -shared flag.
GPT 4.0-mini says that the -fPIC flag (position independent code) is necessary,
but I am not sure why.
cc -shared -fPIC -o build/libhello.so src/libhello.c
Next, compile the program hello.
Compiler needs to know the directory of the library via the -L flag,
and the name of the library via the -l flag.
If the .so file’s name start with lib, then this prefix can be omitted;
if not, then the full name is needed.
cc -I$(pwd)/include -L$(pwd)/build -lhello src/hello.c -o build/hello
# alternatively, specify the full path
cc -I$(pwd)/include -L$(pwd)/build build/libhello.so src/hello.c -o build/hello
When running the program, the dynamic linker needs to know where the dynamic library is.
This can be specified using the LD_LIBRARY_PATH variable:
LD_LIBRARY_PATH=$(pwd)/build:$LD_LIBRARY_PATH ./build/hello
The dynamically-linked binary build/hello is slightly smaller than the statically linked binary: 33432 vs 33464 bytes.
But the program used more CPU:
build/hello 0.00s user 0.00s system 74% cpu 0.007 total