15/10/2022
Just how many times have you cried because of a linker error?
I have. And so many times!
In hindsight, it is a pretty simple topic, but if you're so used to just write code, and let the build system take care of compiling and linking, then you'll probably hit linker errors infrequently enough that you'll be tolerant of the frustration of those moments, as long as some trial and error with build configuration files will get you out of trouble.
But sometimes you can't just hold the frustration anymore, and you command yourself to dedicate some time to go to the bottom of this topic; or, if not to the bottom, at least as low as it takes for you to be happy with it, and no more frustrated by the linking failures.
So here's how I came to write this post: I started by creating
a directory with an evocative name, moved into it,
and started to play with C++ files in an attempt to
reproduce the frustrating workflow to better understand it.
When I started to erroneously delete the wrong file,
to forget what edit I made to file such and such, and
to feel more frustrated because didn't want to spend time
creating a Makefile
to deal with this crap
on my behalf (after all, the Makefile
would
be the guy shielding me away from exactly the topic I
was trying to understand, so probably using one would have
killed the purpose of the exercise), I decided
that writing a post to help myself track the progress of
my understanding was maybe a good idea.
Eventually, this post turned out to be a personally reworded collage of all the info I've found on the web that helped me build a firmer understanding of the compilation process, as well as of the various types of libraries that exist. These two aspects are tightly bounded together, in the sense that libraries (yours or third party's) are exactly the expression of the fact that a program can be written in separate and independently developed, tested, and sometimes built pieces.
(Little but important note: I'm on Linux, both at work and in my personal time, so I will not even try giving Windows examples; but most of this post should be useful anyway.)
The steps from source code to executable
First of all, it is important to have an understanding of what "compiling" a source codes into an executable really means. I've quoted the word "compiling" because proper compilation is just one of the steps that a compiler executes to go from source code to executable program. This short but very good answer on StackOverflow outlines the phases that GCC goes through in the process.
#include
is copy-and-paste
As you might have guessed from its description,
step 1 is what takes care of, among other things,
#include
directives; and you might also
wonder how simple this preprocessing might be,
considering that it happens so early in the process.
And it is indeed a very simple thing:
#include
ing a file is equivalent to
copying-and-pasting it into the includer.
(If you know it already, you can skip the current section.)
And I'm intentionally saying file instead of
header: whatever you #include
is
just copy-and-pasted.
There's truly nothing other than copy-and-paste when you
#include
something.
Indeed, so little is special about #include
, that
if you write a header without having that simple idea in mind, you can easily run in errors
which become obvious, once you are aware that there's just a copy-and-paste going on.
Here's three flawed headers, each defining a different class,
one of which is included by the other two, which are in turn included
in the main
program:
// Foo.hpp
#include "Bar.hpp" // because every
struct Foo { // Foo needs a Bar,
Bar b; // doesn't it?
};
// Bar.hpp
struct Bar {
// whatever
};
// Baz.hpp
#include "Bar.hpp" // Even Baz needs a Bar!
struct Baz {
Bar b;
};
// main.cpp
#include "Baz.hpp"
#include "Foo.hpp"
int main() {
// not even trying to make
// any use of the headers
}
If you click on the last snippet, you'll see that the program
can't be compiled because it results in a redefinition of Bar
,
the reason being that both Foo.hpp
and Baz.hpp
are #include
ing, i.e. copying-and-pasting, Bar.hpp
.
In other words, it's just like we tried to compile this source file alone:
// main.cpp
struct Bar {
// whatever
};
struct Baz {
Bar b;
};
struct Bar { // oops, I did it again
// whatever
};
struct Foo {
Bar b;
};
int main() {
// not even trying to make
// any use of the headers
}
The source above, in fact, is the known as a translation unit,
which is the thing that the compiler compiles (or attempts to,
in this case) and which is roughly "the source file, conventionally named
with extension .cpp
/.cxx
/.c
/whatever, that you feed
to the compiler, in which all #include
s have been copied-and-pasted
(and all other #
-directives have been "resolved", e.g. macro substituted, and so on)"
But then why doesn't this problem occur when we include standard headers?
The answer is simple: the standard headers use a device know as
#include
guards
to prevent multiple inclusions of the same header in the same translation unit,
and you should do so in every single one of your headers.
To see how an include guard looks like, open any system header, such as
iostream
, that on my system is located at /usr/include/c++/12.2.0/iostream
at the time of writing, and you'll see it has this structure:
// comments only
#ifndef _GLIBCXX_IOSTREAM
#define _GLIBCXX_IOSTREAM 1
// the content of the header
#endif /* _GLIBCXX_IOSTREAM */
// EOF
And this is what gets copied-and-pasted in the source includer file, resulting in the translation unit that the compiler tries to compile.
When the compiler reaches the above snippet of code while compiling
a translation unit, it will verify that the macro _GLIBCXX_IOSTREAM
is not defined (because it's #ifndef
, not #ifdef
)
before actually pasting // the content of the header
; it will also, at
that point, #define
the macro that was not defined yet (the value 1
is
irrelevant), so that, should this snippet of code happen to occur again in this same
translation unit, #ifndef _GLIBCXX_IOSTREAM
will not fire and the
// the content of the header
will not be copied-and-pasted again.
Another simpler way of enforcing an
#include
guard, even though not standard, but
practically permitted by all major compilers, is to put
#pragma once
at the top of your header, and nothing else. What's the difference? Practically speaking,
none. Anyway, you can find more info
here
and at the previous link.
Header-only libraries
Knowing that #include
d headers are simply copied-and-pasted,
you can easily imagine you can provide all the functionality you want to a
client of yours by providing them with one or more header files (and by
"header" I mean a proper one, with the #include
guard)
defining all functions and classes they need.
That's not necessarily
a good idea,
but sometimes it's the only option,
for instance when you are defining templates
(unless you already know all possible usages of your template).
Static libraries
One of the drawbacks of header-only libraries is that they
#include
all the headers their implementation needs;
which translates to what is known as code bloat: your translation
unit ends up being huge because of all the direct and indirect
#include
s. Indeed, in the example above, you can well see
that the code inside <iostream>
gets pasted into
main.cpp
, even though main.cpp
doesn't
need to know anything about <iostream>
; all it
needs to know is how to call foo
, i.e. it needs to
know its signature.
Whether you should fear this problem and how much is another matter that this post does not address at all, and it is influenced, among other things, by how smart the compiler is.
Anyway, what is the solution? There's basically no solution if you provide
templates that will be instantiated with types you don't know of, and
that's why templates are often accused of causing code bloat (see also
§9.1.1 from C++ Templates - The Complete Guide - 2nd edition for more
details), but for the simple case of non-template entities, such
as the foo
function above,
the solution is simple enough.
Indeed, all main.cpp
needs to know about foo
in order to be able to call it, is that it takes void
and returns void
, i.e. that its type is void(void)
,
or more simply void()
. And all we need to do to make it aware
of this fact, is writing in it a declaration of foo
,
i.e. void foo();
.
As you might have guessed, we don't do that manually, but by
including an header which contains that declaration.
So here's our solution in code:
-
MyLib.hpp
declaresfoo
,// MyLib.hpp #pragma once void foo();
-
MyLib.cpp
defines it,// MyLib.cpp #include "MyLib.hpp" void foo() { std::cout << "Hello" << std::endl; }
-
and
main.cpp
#include
s the header and usesfoo
.// main.cpp #include "MyLib.hpp" int main() { foo(); }
If you're wondering why MyLib.cpp
#include
s its own header MyLib.hpp
the answer is that
there is such a myriad of scenarios where doing so is necessary
(e.g. MyLib.hpp
defines an inline
variable to be used in client code as well as in the
implementation of some other exposed function), that
not doing it in a rare scenario like the one above
(more likely to happen in toy examples than in real code)
is just not worth the risk of forgetting it when it's needed.
You can read more about it
in this question I asked on
StackOverflow and in the
associated answers.
The above example can be built like this
g++ -c main.cpp -o main.o # compile main.cpp
g++ -c MyLib.cpp -o MyLib.o # compile MyLib.cpp
g++ main.o MyLib.o -o main # link
where the first two commands can be executed in any order,
because the translation units can be compiled independently,
thanks to the fact that the only thing that main.cpp
needs to know about MyLib.cpp
, i.e. the declaration
of foo
, has indeed been "pasted" into main.cpp
as well, via the inclusion of MyLib.hpp
.
Going back to the building process,
once again, it can be executed in one step,
but I think it's instructive to see what happens if you try linking
without telling the linker where MyLib.o
is. What you get
is an error from the linker, ld
, complaining about
an undefined reference to foo()
:
$ g++ main.o -o main # forgot MyLib.o
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x5): undefined reference to `foo()'
collect2: error: ld returned 1 exit status
And maybe it's even important to know how this error differs
from the one you get if you forget to #include "MyLib.hpp"
in main.cpp
, which complains about
foo
being not declared. This is an
error that happens at compile-time, rather than link time,
because main.cpp
doesn't even know foo
's
signature.
$ g++ -c main.cpp -o main.o
main.cpp: In function ‘int main()’:
main.cpp:3:5: error: ‘foo’ was not declared in this scope
3 | foo();
| ^~~
(More precisely, the error above happens during
the compilation to assembly, i.e. you get the same
error if you run the command $ g++ -S main.cpp -o main.s
.)
At this point one question that might arise is:
where did the code in MyLib.cpp
end up
being? You can verify, by getting rid of the *.o
files, that main
works well in isolation:
$ rm MyLib.o main.o
$ ./main
Hello
Therefore,
you can also be sure that all the code it needs, including
the one originally in MyLib.cpp
and compiled
in MyLib.o
, is indeed
embedded into the binary named main
.
In other words, the code of a static library becomes
part of the executable file at link time.
Dynamic libraries
So we've verified that when linking a static library
against a client code, the library's and client's code end up in the same produced executable.
Therefore, if the static library changes, i.e. if its author changes MyLib.cpp
,
recompiles it into MyLib.o
and gives the latter to you, you still have
to re-link your main.o
against the new MyLib.o
, otherwise you'll
be still using the old one.
Is there a way that the library writer can compile the library such that they're free to change the implementation of the library without forcing us to re-link? Yes there is.
Let's make just one little change to the workflow described above. The source and header files stay the same, but we compile the library with a different command:
g++ -fPIC -shared MyLib.cpp -o libMyLib.so
Then we link using the same command as before:
$ g++ main.o libMyLib.so -o main
If we try to run the executable straight away as we did earlier, though, we get an error,
$ ./main
./main: error while loading shared libraries: libMyLib.so: cannot open shared object file: No such file or directory
This error tells us two things:
- "
error while loading shared libraries
" tells us that linking doesn't embed the library executable code into themain
executable; - "
No such file or directory
" tells us that the runtime system that attempted to start the program couldn't find the library, so we need a way to instruct it about where to look for it.
The solution here is easy: we can pass the missing
information via the LD_LIBRARY_PATH
macro,
$ LD_LIBRARY_PATH='.' ./main # . is the current dir, where the library is
Hello
Now it should be clear that if we delete MyLib.so
and run main
, we get the same error as before,
$ rm MyLib.so
$ LD_LIBRARY_PATH='.' ./main
./main: error while loading shared libraries: libMyLib.so: cannot open shared object file: No such file or directory
but a more interesting observation is that we can
change MyLib.cpp
(but leaving its interface
intact, i.e. we don't change MyLib.hpp
),
recompile the library, and executing main
will
reflect those changes, without any need to re-link:
$ sed -i 's/Hello/Bye/' MyLib.cpp # change Hello to Bye (you can do it any other way)
$ g++ -fPIC -shared MyLib.cpp -o libMyLib.so # recompile only the library
$ LD_LIBRARY_PATH='.' ./main
Bye
Therefore, an author of a dynamic library
can do all the changes they like to the implementation of the
library (but not to the interface they promised), share the
binaries with the client, and the client will just have to
overwrite the old shared objects with the new ones, and the
executable will pick them up. In other words, the code
of a dynamic library becomes part of your program at
runtime startup time.
And this means the *.so
file must be there when
you run main
, even if main
doesn't
make use of it (try removing the call to foo()
from
inside main.cpp
, recompile and relink it against
libMyLib.so
, then delete libMyLib.so
,
run LD_LIBRARY_PATH='.' ./main
and you'll get same error as above).
Why have I specifically written "startup time"
rather than "runtime"? Because the runtime system
loads the library even before the actual program starts
executing. How can we verify this? Simple: put a print
statement in main.cpp
before the call to
foo
, like this
// main.cpp
#include "MyLib.hpp"
int main() {
std::cout << "before" << std::endl;
foo();
}
and then
$ g++ -c main.cpp -o main.o
$ g++ main.o libMyLib.so -o main
$ rm libMyLib.so
$ LD_LIBRARY_PATH='.' ./main
./main: error while loading shared libraries: libMyLib.so: cannot open shared object file: No such file or directory
As you can see, no trace of the text before: the executable hasn't even started.
This also means, though, that once the executable has started,
you can indeed delete the .so
file, and the
running process will not be impacted, which you can verify by
repeating the above process, but changing this
std::cout << "before" << std::endl;
to this
int dummy;
std::in << dummy;
re-compiling, re-linking, starting the program, deleting
libMyLib.so
while the executable is waiting for
your input, and then entering the input. The program will do
just fine.
At this point, if you cannot guess already what the next section is about, I think you probably will if I ask you two questions:
- How much does loading a library cost?
- How many libraries are we realistically loading in a non-toy application?
Clearly, the answers vary wildly across the use cases, but the point is that loading a library does have a cost, and for big programs, especially those with multiple functionalities that are not all necessarily needed at the same time (or not at all if the user chooses not to use them in a session of the program), there can be many of those costs; as many that they can add up at the expense of the startup time of the program and, consequently, of the user experience.
You might now be wondering whether a way exists to load a dynamic library only when it is needed. This is the question that leads us to the final section of this post…
Lazily loaded dynamic libraries
If the library is not loaded by the runtime system as soon as we start the program, then who takes of care of loading it? There's no other answer than "the program" itself. After all, that's what we want: the program logic to decide when and if to load the library. And the API to do so is part of the GNU C library.
Before showing the code, I think it is important to stress a few points:
- we already have a dynamic library in place, and from the perspective of the clients of the library, that's not gonna change;
- the library implementer can change the implementation of it
(i.g. the
MyLib.cpp
file), but as long as they keep the interface (i.e. theMyLib.hpp
) unchanged, for the clients it's just like the library doesn't change; - the lazy or eager policy in loading the library is a decision made on the client side, which the implementer of the library knows nothing about.
That being said, it should be clear that we will now only play the role of the library clients, with the aim of loading the library lazily, during run time, rather than eagerly, during launch time. For this reason, we'll only have to review two things:
-
the actual code of our application,
main.cpp
, to make it actually handle the loading of the library, - and the command to build our application (which is fairly straightforward, if not unsatisfying).
As regards the first point, main.cpp
is
now a bit more complicated, because it needs to deal with the
possibility of failure when loading the library and/or
reading the symbol(s) we are looking for, but the comments
should make most of it understandable.
#include <iostream>
#include <dlfcn.h>
#include "MyLib.hpp"
int main() {
// try opening the library
void * lib = dlopen("./libMyLib.so", RTLD_LAZY);
// verify opening was fine, or error out
if (!lib) {
std::cerr << "Error (when opening the library \"./libMyLib.so\"): " << dlerror() << std::endl;
return 1; // if we return here, echo $? in the shell will print 1
}
// so far so good
dlerror(); // reset the error status
// try loading the symbol `foo`
void * fooPtr = dlsym(lib, "foo");
auto error = dlerror();
// verify loading symbol was fine, or error out
if (error) {
std::cerr << "Error (when loading the symbol `foo`): " << error << std::endl;
return 2; // if we return here, echo $? in the shell will print 2
}
// so far so good
dlerror(); // reset the error status
// cast foo to the function type we know and call it
using Foo = decltype(foo);
((Foo*)fooPtr)();
}
As regards the second point, the solution is simple: we just don't link against the library, because we are delegating the task of "establishing a connection" with the library to our program itself.
However, surprisingly enough (at least for those of us who are new to this topic) …
$ g++ main.cpp -o main # compile and link main
$ g++ -fPIC -shared MyLib.cpp -o libMyLib.so # compile the library
$ ./main # run
Error (when loading the symbol `foo`): ./libMyLib.so: undefined symbol: foo
$ echo $? # print last return status
2
… the runtime doesn't seem to find the symbol foo
,
even though the library loading succeeds (indeed, we exited with
return 2
, not return 1
).
Without dedicating enough time to think about why that was happening,
I rushed at asking on StackOverflow
to discover something which is obvious in hindsight:
since in C++ functions can be overloaded, the name of function
alone is not enough, in general, to unequivocally address
what could be one of many overloads.
To know what function to call, the linker has to know the
function signature too, so the compiler has to provide it in
the object file, and it does so by presenting each overload
of foo
with a differently mangled name (the thing
I mentioned earlier), where
the mangling encodes the signature. In short, if we change
void * fooPtr = dlsym(lib, "foo");
to
void * fooPtr = dlsym(lib, "_Z3foov");
the problem is solved.
This solution, however, is non-portable, as noted in
a comment
to the linked question. In the case that the library provides
a single overload of foo
, then an alternative solution
(from the perspective of the library writer) is to declare foo
like this
extern "C" void foo();
so its name will not be mangled in the shared object file
(in C there's no function overloading, hence no reason to
mangle names), and we can pass just "foo"
to
dlsym
.
If, instead, the library provides more overloads,
then extern "C"
is not a viable solution, and
one has to deal with mangled names. I'm not sure a portable
solution exists, but
here's
a relevant question with some possible solution outlined (the
lengthy comments to the question are very informative, so I
suggest to have a read of those too). Incidentally, I'm not
sure how much the necessity of inspecting a library for mangled
names has to do, if anything at all, with the question of
whether the application should still include a header
from the library (there's an hint of this aspect of the matter
in the comments
here).
Leaving aside the topic of shared libraries providing overloaded
functions, and staying in the realm of what is possible with extern "C"
(or, alternatively, writing non-portable code by hard coding mangled names
in the client code),
it should be clear that with such a mechanism (I mean, dlopen
+dlsym
,
not only can we load a library lazily, i.e. only when its needed,
but we can also take into account the scenario where the library
is missing, and provide a fallback alternative to it.
Below I've included one such example. It it is made up of
- a
main.cpp
that tries to load a librarylibMyLib.so
and, if that is not available, falls back on a default library; - a default library
DefaultLib.cpp
/DefaultLib.hpp
; - a
libMyLib
library,MyLib.cpp
/MyLib.hpp
.
It can be compiled and used like this:
$ g++ -c main.cpp -o main.o # compile main
$ g++ -c DefaultLib.cpp -o DefaultLib.o # compile default library
$ g++ DefaultLib.o main.o -o main # link
$ ./main # run (libMyLib.so is not available)
Slow, old, rotten version
$ g++ -fPIC -shared MyLib.cpp -o libMyLib.so # compile shared library
$ ./main # run (libMyLib.so is available)
Fast version (you've I paid big money for it)
Here you can see that main
can be run
with or without the shared library being available,
and its logic will act accordingly. (By the way, we
don't need to prepend LD_LIBRARY_PATH='.'
because we load the library from where we know it is,
see "./libMyLib.so"
in the code below.)
Below are the source codes.
// DefaultLib.hpp
#pragma once
namespace old {
void foo();
}
// DefaultLib.cpp
#include "DefaultLib.hpp"
#include <iostream>
namespace old {
void foo() {
std::cout << "Slow, old, rotten version" << std::endl;
}
}
// MyLib.hpp
#pragma once
extern "C" void foo();
// MyLib.cpp
#include "MyLib.hpp"
#include <iostream>
void foo() {
std::cout << "Fast version (you've I paid big money for it)" << std::endl;
}
// main.cpp
#include "DefaultLib.hpp"
#include "MyLib.hpp"
#include <dlfcn.h>
#include <iostream>
int main() {
// try opening the library
void * lib = dlopen("./libMyLib.so", RTLD_LAZY);
// verify opening was fine, or use default version
if (!lib) {
old::foo();
return 0;
}
// so far so good
dlerror(); // reset the error status
// try loading the symbol `foo`
void * fooPtr = dlsym(lib, "foo");
auto error = dlerror();
// verify loading symbol was fine, or use default version
if (error) {
old::foo();
return 0;
}
// so far so good
dlerror(); // reset the error status
// retrieve type of foo, as we expect it based on the header
using Foo = decltype(foo);
// and cast fooPtr to a pointer to that type before using it
((Foo*)fooPtr)();
}
Click on the last one to see the complete example
on Compiler Explorer; comment the add_library(…)
line in the CMakeLists.txt
file to hide the library, thus
triggering the use of the default one; also, consider that I
had to change "./libMyLib.so"
to
"build/libMyLib.so"
because that's where CE puts
the shared object; I didn't know,
I just asked.
Yeah, but how do I debug library code? — Demo
As far as header-only libraries are concerned, debugging is just as you would expect: you have the library code, you include it into yours, you compile the way you like, e.g. enabling debugging flags, and you step into the library code just as it was your own code. Easy-peasy.
As regards the other types of libraries we've reviewed, i.e. static and dynamic libraries, those normally come in compiled form (shared object, library archives, … whatever), is simply no C++ source code you can step through, … unless you do have the source code from which the library was compiled. So we can say that the possible scenarii are the following:
- The library is not open source. In this case you just don't and can't legally have the source code, so you just can't step through it with a debugger. Is is a problem? No, as most likely you paid for the library, so you can report a bug and expect that the vendor will debug and fix the bug.
- The library is open source and you installed it from pre-compiled binaries. In this case you can't step trough the code because, just as in case 1, you don't have it.
-
The library is open source and you have built it from
source on your system. In this case you do have the
source code you want to step through, with one caveat:
most likely the library was built with full
optimizations on, in order to target performance, so
you'll probably see a lot of jumps when debugging, and
you'll not be able to set breakpoints on every single
executable line of code, because many have been optimized
always. If that's the case, you might want to
(temporarily) recompile the library with debug flags on
(e.g.
-g -O0
with GCC) before debugging.
A tricky bit about debugging a non-header-only library, though, is that the debug symbols have to be loaded during the debug session. Depending on what debug adapter you use, this could happen automatically or not, and the behavior of the same debug adapter can likely depending on how you configure it.
In the following screen cast I'll show how I step through
the last example with the debugger; initially I'll use the
vscode-cpptools
adapter with setting
"symbolLoadInfo": { "loadAll": false, "exceptionList": ""}
,
which fundamentally means "do not load debug symbols from
libraries, and make no exception", so you'll see that I
need to manually load the symbols from the library to enable
a breakpoint in that library code. Then I'll show you the
same workflow with
CodeLLDB,
which, for some reason I don't know, ignores the setting I
mentioned earlier (maybe it uses another syntax and ignores
this one because it doesn't understand it?) and loads the
symbols as soon as dlopen
returns.
Conclusions and summary
Creating all the examples above and playing with each of them helped me have a better understanding of what compiling and linking actually are, how different parts of a program know about each other, and what the difference is between static and dynamic libraries.
Here are the main points.
#include <foo>
just means "copy-and-paste the filefoo
right here".- A header-only library is a library which is made up
entirely of… headers, which you
#include
in your files, and that becomes part of your program as soon as the preprocessing stage of the building process happens. - A static library is an already compiled piece of code
(often in the form of an object file,
someObj.o
, or of an archive,someLib.a
) which you link against your compiled program; the result is one executable that contains everything it needs. - A dynamic library is, too, an already compiled piece of code, but at link time it doesn't become part of the generated executable; the latter, in fact, only has references to the entities it needs from the library, and those references are resolved when your program is launched.
- A dynamic library, alternatively, can be loaded at
run time by the program itself, via
dlopen
anddlsym
, removing entirely the necessity of liking your program against it.
Referenced sources
Below are most of the sources (some of which already linked off the text above) I found invaluable in helping me understanding the topic I've been writing about today.
- Dynamic Loading Without
extern "C"
- Is there any reason I should include a header in the associated cpp file if the former only provides declarations the latter defines?
dlopen
succeeds (or at least seems to) but thendlsym
fails to retrieve a symbol from a shared library- Loading shared library dynamically using
dlopen
- Difference between shared objects (
.so
), static libraries (.a
), and DLL's (.so
)? - How to use Libraries
- What's the difference between object file and static library(archive file)?
- When are header-only libraries acceptable?
- How dynamic linking works, its usage and how and why you would make a
dylib
- Linux error while loading shared libraries: cannot open shared object file: No such file or directory
- How do I list the symbols in a
.so
file - What do 'statically linked' and 'dynamically linked' mean?
gcc
Linkage option-L
: Alternative ways how to specify the path to the dynamic library- Static Vs Dynamic libraries