C Builds and Libraries for the Inexperienced
This guide was made for /agdg/, /gedg/, and /chad/! If you see any glaring errors, feel free to tell Frosch, he would appreciate the criticism to make this tutorial better.
In this world if anything is eternal, it is the learning curve of building C projects. The problem is that, like any subject, people turn something easy into something hard by skipping fundamental steps. This might not be your fault. You might ask: "How do I use a library?", and someone answers with "CMake". And while CMake is useful, it is NOT what you should be using for libraries the very first time you learn about them. It is important to build and link a simple library using the compiler before trying it in CMake. That being said, you might have to do a lot of reading, but none of this is actually hard to do.
This tutorial assumes you already know how to build and run small programs like "Hello World" from the command line and that you understand how to read and write code in the C language. The reader should definitely have some command line literacy and be able to clone repositories with git.
I also assume you are using a GNU/Linux distro and the programs listed. If not, you will need to find the equivalent programs and follow the equivalent steps in those programs. I don't assume you've built a project that used a library, outside of the standard library, which your typical compiler handles for you automatically.
We will do different build methods for many of the sections in this order:
- gcc, the GNU compiler collection
- CMake
I recommend that you read the whole thing. (Even the first section, trust me.)
Howdy World
In the command line run
wherever you like to keep your projects. If you don't want to use vim, then use your preferred text editor. The contents of the file should look like this:
gcc
The gcc compiler goes through four stages in order to build the program.
-
Preprocessing
- Removes comments in the code.
- Handles preprocessor directives that start with '#' such as including header files (.h) and macros.
To stop after preprocessing, type out
-
Compiling
To stop after compiling, do
yields
Now lets clean up that file with
Sometimes people refer to the whole build process as compiling, which can make things confusing.
-
Assembling
Assembling takes the intermediate representation, and converts it into machine language, saved in what is called an object file.
To stop building after the assembling stage:
This time, we are using the
-o
output flag which needs to take in a path. With the goal of organization, we want to place the object file in a directory instead of just spawning it wherever we run the command. Keep in mind that the other compiler stage flags that we've seen so far ('-E', '-S', '-c') don't have an input. So you might also see it typed like-c src/main.c
for example. -
Linking
Here we glob all the object files together and produce a binary. In this case, gcc automatically links the object code of the standard library with the object code we made. So lets link our object file from the previous step.
Now run the program with
./bin/"Howdy World"
which prints
If you want, you can delete the object file with
rm -r obj
after you have your binary.
People often skip step by step compilation without ever trying it once.
THIS IS BAD!
When we discuss libraries later, we'll see the importance of stopping the build process before the linking stage. Of course, now that you understand that the compiler has these different stages, there is no harm running
to do all four steps and then run the program, printing the same result. For our purposes, if there is anything you need to take away from this, it's that it is useful to understand how to stop the build process before the linking stage.
CMake
In order to make the build process easier for large projects, makefiles can be used to specify build details. The reason why you would want one is not obvious yet, because so far this program compiles easily in one line. A cool thing about makefiles is that you can break a project up so that only the files that are altered have to be recompiled.
I won't go through creating makefiles from scratch, but we will use CMake to create makefiles for us. If you want a general idea of how a makefile works, you can look at an example with C++ here. GNU Make is seriously awesome software, and I do highly recommend that you learn it, but I think there are better sources for that. So as a result, this tutorial will be concerned with CMake. That being said, any time we create a makefile with CMake, you can always investigate it to get a better idea about how it works. For an in depth look, I refer you to the official GNU Make documentation.
CMake involves the use of high-level 'targets' that represent libraries and binaries as well as our own custom targets. To get our feet wet, we should try using CMake for the most basic of examples. First lets clean up what we built with gcc.
Next, we need to create a CMakeLists file.
The contents are
- Specify the minimum required version at the very top of the file. You can check your current CMake version by typing
cmake -version
in the command line. - Now look at the second line. CMake gives us access to a top-level project name variable separate from the executable target. This makes sense because a CMake project can be a collection of different targets.
- The last line creates an executable target. This allows us to make our binary. CMake by default identifies the 'build tree' in the directory where you run the
cmake
command.CMakeCache.txt
is generated in this directory, and it is also the default location for the binary .
Now lets build the project!
The dot is used to tell CMake to look in the current directory for CMakeLists.txt
. Specifying the location of this file is required with CMake, and you can not have more than one CMakeLists file in a single directory. On the other hand, make will automatically search in the current directory for the makefile.
If we run the binary with
we get a familiar message. Now you might notice that a lot of things about this setup are crappy. Let us print the contents of the project directory.
The output
is not very pretty. That's because our build tree is just sitting in the same directory as everything else. Let's clean up all this clutter.
We want CMake to do its business somewhere else. Try
Now all that junk will be isolated to our build directory. We run cmake ../
in the build directory so that CMake identifies the build directory as the location of the build tree, but searches forCMakeLists.txt
in the parent directory. However, now the binary is located in the build folder. Instead you may want to put that binary in a different, dedicated place. Lets edit our CMakeLists to see how we do that.
Notice the additional line. By default, the binary is given the same name as the executable target. However, there are certain rules about the allowed characters in a CMake target name. For example, we can't put a space in the name or use '/' for a different path. Fortunately, we can explicitly set the binary's output path by setting the target's properties. For the OUTPUT_NAME
property, we specify the full path relative to the project source directory. Now CMake knows where to put the binary, regardless of where the build tree is located.
Make sure to remove the old binary and then make a directory for the new binary. It's time to repeat the build process.
Nice! Looks like things are organized now. However, typing all these commands over and over again to rebuild the project is annoying. We would have to type all those commands anytime we changed the source code! It would behoove us to automate these commands with a script. On GNU/Linux, you can make a build script in bash and give it permission to execute.
The contents should look like this:
With scripts we don't have to cd
back to where we started. Next delete the binary and build directories, then try running the script from some arbitrary directory to make sure it works.
Project with Two Source Files and a Header File
In the terminal, cd
to wherever you like to keep your projects, and run the following:
While I personally don't like putting my headers in another directory, some people do. We are also doing this so we can understand how to include header files from some other directory, a skill we'll need once we get to libraries anyways. Using a text editor, add the following contents to the appropriate files.
include/add.h
src/add.c
src/main.c
Note that we instructed the source files to include add.h
not ../include/add.h
. We are going to give build instructions to handle this.
gcc
Keep in mind, we are not linking with any library other than the C standard library, so we can choose to do the whole build process in one command.
We use the flag -I
to specify that the contents of the directory include
should be treated as if they were in the same directory as every source file. Some people prefer -Iinclude
over -I include
; the flag ignores the space, so do what you want.
Now run the program with ./bin/add
, and see that 4 + 5 = 9.
CMake
Copy over your reliable CMake build script "Howdy World"/build.sh
to sum/build.sh
so you can reuse it. After copying, make sure you are in sum
before we start. Now lets create our CMakeLists file.
Inside, you should have
CMakeLists.txt
It is important that you use target_include_directories after you create the executable target. We are setting the include directories just like the -I
flag we used with gcc. Now build and run the program.
and see that 4 + 5 = 9 yet again.
Before moving forward I think its a good idea to touch on a big benefit of using CMake. Imagine instead of our two source files, we had a hundred, all included in our source directory or some subdirectory. And so you likely automate the build process with a script, which, to be fair is easy to do or Google how to do. Lets also say that your program needs to be used on more than one operating system. So when you make changes to your build you'll have to reimplement that bullshit all over again for the other OS that you are likely less familiar with.
However, CMake is cross-platform, and you can just make short scripts to build for each platform, or even just have the user do it themselves without a convenience script (which is very common). And even if you aren't trying to make something cross-platform, we'll see in a later section that other libraries often use it. CMake will allow you to easily use those libraries in your own project.
The following CMakeLists will search recursively through every file in the directory and every potential subdirectory for all source files. Which means anytime you add a source file to that directory, you just need to rebuild with build.sh
.
CMakeLists.txt
Try moving your main source file to some subdirectory.
Rebuild and rerun the program to make sure it works!
Then clean up.
One Function Static Library
Before moving forward I highly recommend viewing the links below. In particular, try to get an idea about what libraries are and how we use them.
https://youtu.be/JbHmin2Wtmc
https://youtu.be/_kIa4D7kQ8I
https://stackoverflow.com/a/9688536
We are going to reuse all the C code from earlier, and just move things around. In your projects directory
Now copy files from the last project into the new project.
sum/src/main.c
intosumlib/src/main.c
sum/src/add.c
intosumlib/lib/src/add.c
sum/include/add.h
intosumlib/lib/include/add.h
What we are going to be making is a static library. The idea is to seperately compile and archive some object code that we can link with other object code that contains the entry point (the main()
function). For static libraries, a copy of the library exists within the final binary.
gcc
After copying the files make sure you are in sumlib
before we start. First lets make the object file for our entry point; so we need to stop before the linking stage in the build process. Do you remember how we did things in the 'Howdy World' program?
Remember, we include add.h
in main.c
as if they were in the same directory, so we have to use the flag -I
.
To build a library we archive all the library's object files (in this example we only have one) together with ar
. If you've ever compressed a file into a zip,rar or whatever, this should be somewhat familiar.
An important rule of C libraries is that they must start with 'lib'! For a static library, make sure to use the '.a' extension.
Now we can link it! To link a library, use the -l
flag followed by the name of the library. To tell the compiler where to find libraries, use the -L
flag.
Note that when we want to link 'libsum.a' we just type out 'sum'. Because we linked a static library, the library code has been incorporated into our binary. If we want we can get rid of it and all the other object files.
The program will run all three times. Keep this in your head for when we move on to shared libraries after this next CMake section.
CMake
Now copy the build script from the earlier projectsum/build.sh
into sumlib/build.sh
and clean up.
The way we'll handle things is by creating two CMakelists files, a main file and a file in the lib
directory.
The contents are
lib/CMakeLists.txt
CMakeLists.txt
The CMakeList file in the directory lib
creates a CMake library target. Then we tell the main CMakeLists file where to find the other file with add_subdirectory
. The last two lines of the main file tells us where to find the libraries and which libraries to link. This should remind you of the -L
and -l
gcc flags.
yields the familiar result. Take a peak in build/lib
and you should see the library. Try deleting it and running the program again to see that the program still runs. Keep this in mind for the next section.
One Function Shared Library
In your project directory duplicate the static library project
gcc
This is a Linux leaning guide, so in this gcc section we'll just cover shared libraries (.so) and not dlls. We start by compiling main.o.
Next we create the shared library.
Compare this to the similar step when creating the static library. Note that we use .so instead of .a, that we use gcc with the -shared
flag instead of ar
, and that we use the flag -fPIC
(position-independent code). Then we link the shared lib.
Now if you are overeager and try running ./bin/add
you will get the following error:
That is because we need to indicate where the shared library is unless it is installed in some default location (like /usr/lib, for example). Try echo $LD_LIBRARY_PATH
, we need to set this environment variable to where our lib is. Try running the following in the command line and compare it to the static lib version of our lib.
The program only works the first two times. Unlike a static lib, we need the shared library at runtime!
cmake
Now clean up from the previous section.
We need to edit the the library CMakelists file that we copied over from the static lib project.
lib/CMakeLists.txt
So easy, all we did is add 'SHARED' ! On Windows this will create a dll instead of a non-Windows (.so) shared library.
And just like that we go from a program with static linking to one with dynamic linking! Take a peak in build/lib
and you should see the library. Try deleting it and running the program again to see that the program won't run. Compare this to when we deleted the static library previously.
Howdy Window with raylib
Now lets use a library that someone else made. I'm choosing raylib because it shouldn't have any dependencies we have to worry about. raylib actually offers build options with a CMakeLists file or a makefile. A lot of libraries take into consideration the user and include some method to ease or automate the build process. For this tutorial, rather than use the makefile, we are just going to build everything with CMake. If we use what we've learned, it will be EZPZ.
Go to your projects directory. Then
You should now have a directory called raylib
. Following the basic example from the website, lets have the window print something for us.
Inside the source file you should have
An investigation of the raylib source code and this documentation reveals that the header files and library can both be found inraylib/src
and the CMakeLists file is located in raylib
.
So enter the command vim CMakeLists.txt
, and inside it add
Copy over the build script that we've been using this whole tutorial. Now run the build script and then run the binary to see your very own raylib window!
Note: Be careful, in this case the sub-directory and library have the same name. This often occurs with libraries if you don't alter the directory name after cloning. Please don't mix up these two CMake commands! One thing you can do to avoid confusion is git clone or git submodule into a subdirectory like 'external' or 'vendor'.
Afterward
I didn't want this tutorial to be too long (at least not for those early drafts), but I'll add things as the ideas come to me.
TODO :
- Preprocessor stuff, in particular defining macros with gcc and CMake. Maybe give an example on how to set up a build and debug configuration with a logger.
- System libraries and find_package()