
Static libraries
As we have explained before, a static library is one of the possible products of a C project. In this section, we are going to talk about static libraries and the way they are created and used. We will then continue this discussion by introducing dynamic libraries in the next section.
A static library is simply a Unix archive made from the relocatable object files. Such a library is usually linked together with other object files to form an executable object file.
Note that a static library itself is not considered as an object file, rather it is a container for them. In other words, static libraries are not ELF files in Linux systems, nor are they Mach-O files in macOS systems. They are simply archived files that have been created by the Unix ar
utility.
When a linker is about to use a static library in the linking step, it first tries to extract the relocatable object files from it, then it starts to look up and resolve the undefined symbols that may be found in some of them.
Now, it's time to create a static library for a project with multiple source files. The first step is to create some relocatable object files. Once you have compiled all of the source files in a C/C++ project, you can use the Unix archiving tool, ar
, to create the static library's archive file.
In Unix systems, static libraries are usually named according to an accepted and widely used convention. The name starts with lib
, and it ends with the .a
extension. This can be different for other operating systems; for instance, in Microsoft Windows, static libraries carry the .lib
extension.
Suppose that, in an imaginary C project, you have the source files aa.c
, bb.c
, all the way up to zz.c
. In order to produce the relocatable object files, you will need to compile the source files in a similar manner to how we use the commands next. Note that the compilation process has been thoroughly explained in the previous chapter:
$ gcc -c aa.c -o aa.o
$ gcc -c bb.c -o bb.o
.
.
.
$ gcc -c zz.c -o zz.o
$
Shell Box 3-8: Compiling a number of sources to their corresponding relocatable object files
By running the preceding commands, we will get all the required relocatable object files. Note that this can take a considerable amount of time if the project is big and contains thousands of source files. Of course, having a powerful build machine, together with running the compilation jobs in parallel, can reduce the build time significantly.
When it comes to creating a static library file, we simply need to run the following command:
$ ar crs libexample.a aa.o bb.o ... zz.o
$
Shell Box 3-9: The general recipe for making a static library out of a number of relocatable object files
As a result, libexample.a
is created, which contains all of the preceding relocatable object files as a single archive. Explaining the crs
option passed to ar
would be out of the scope of this chapter, but in the following link, you can read about its meaning: https://stackoverflow.com/questions/29714300/what-does-the-rcs-option-in-ar-do.
Note:
The ar
command does not necessarily create a compressed archive file. It is only used to put files together to form a single file that is an archive of all those files. The tool ar
is general purpose, and you can use it to put any kind of files together and create your own archive out of them.
Now that we know how to create a static library, we are going to create a real one as part of example 3.2.
First, we are going to presume that example 3.2 is a C project about geometry. The example consists of three source files and one header file. The purpose of the library is to define a selection of geometry related functions that can be used in other applications.
To do this, we need to create a static library file named libgeometry.a
out of the three source files. By having the static library, we can use the header file and the static library file together in order to write another program that will use the geometry functions defined in the library.
The following code boxes are the contents of the source and header files. The first file, ExtremeC_examples_chapter3_2_geometry.h
, contains all of the declarations that need to be exported from our geometry library. These declarations will be used by future applications that are going to use the library.
Note:
All the commands provided for creating object files are run and tested on Linux. Some modifications might be necessary if you're going to execute them on a different operating system.
We need to take note that future applications must be only dependent on the declarations and not the definitions at all. Therefore, firstly, let's look at the declarations of the geometry library:
#ifndef EXTREME_C_EXAMPLES_CHAPTER_3_2_H
#define EXTREME_C_EXAMPLES_CHAPTER_3_2_H
#define PI 3.14159265359
typedef struct {
double x;
double y;
} cartesian_pos_2d_t;
typedef struct {
double length;
// in degrees
double theta;
} polar_pos_2d_t;
typedef struct {
double x;
double y;
double z;
} cartesian_pos_3d_t;
typedef struct {
double length;
// in degrees
double theta;
// in degrees
double phi;
} polar_pos_3d_t;
double to_radian(double deg);
double to_degree(double rad);
double cos_deg(double deg);
double acos_deg(double deg);
double sin_deg(double deg);
double asin_deg(double deg);
cartesian_pos_2d_t convert_to_2d_cartesian_pos(
const polar_pos_2d_t* polar_pos);
polar_pos_2d_t convert_to_2d_polar_pos(
const cartesian_pos_2d_t* cartesian_pos);
cartesian_pos_3d_t convert_to_3d_cartesian_pos(
const polar_pos_3d_t* polar_pos);
polar_pos_3d_t convert_to_3d_polar_pos(
const cartesian_pos_3d_t* cartesian_pos);
#endif
Code Box 3-3 [ExtremeC_examples_chapter3_2_geometry.h]: The header file of example 3.2
The second file, which is a source file, contains the definitions of the trigonometry functions, the first six functions declared in the preceding header file:
#include <math.h>
// We need to include the header file since
// we want to use the macro PI
#include "ExtremeC_examples_chapter3_2_geometry.h"
double to_radian(double deg) {
return (PI * deg) / 180;
}
double to_degree(double rad) {
return (180 * rad) / PI;
}
double cos_deg(double deg) {
return cos(to_radian(deg));
}
double acos_deg(double deg) {
return acos(to_radian(deg));
}
double sin_deg(double deg) {
return sin(to_radian(deg));
}
double asin_deg(double deg) {
return asin(to_radian(deg));
}
Code Box 3-4 [ExtremeC_examples_chapter3_2_trigon.c]: The source file containing the definitions of the trigonometry functions
Note that it is not necessary for sources to include the header file unless they are going to use a declaration like PI
or to_degree
, which is declared in the header file.
The third file, which is a source file again, contains the definitions of all 2D Geometry functions:
#include <math.h>
// We need to include the header file since we want
// to use the types polar_pos_2d_t, cartesian_pos_2d_t,
// etc and the trigonometry functions implemented in
// another source file.
#include "ExtremeC_examples_chapter3_2_geometry.h"
cartesian_pos_2d_t convert_to_2d_cartesian_pos(
const polar_pos_2d_t* polar_pos) {
cartesian_pos_2d_t result;
result.x = polar_pos->length * cos_deg(polar_pos->theta);
result.y = polar_pos->length * sin_deg(polar_pos->theta);
return result;
}
polar_pos_2d_t convert_to_2d_polar_pos(
const cartesian_pos_2d_t* cartesian_pos) {
polar_pos_2d_t result;
result.length = sqrt(cartesian_pos->x * cartesian_pos->x +
cartesian_pos->y * cartesian_pos->y);
result.theta =
to_degree(atan(cartesian_pos->y / cartesian_pos->x));
return result;
}
Code Box 3-5 [ExtremeC_examples_chapter3_2_2d.c]: The source file containing the definitions of the 2D functions
And finally, the fourth file that contains the definitions of 3D Geometry functions:
#include <math.h>
// We need to include the header file since we want to
// use the types polar_pos_3d_t, cartesian_pos_3d_t,
// etc and the trigonometry functions implemented in
// another source file.
#include "ExtremeC_examples_chapter3_2_geometry.h"
cartesian_pos_3d_t convert_to_3d_cartesian_pos(
const polar_pos_3d_t* polar_pos) {
cartesian_pos_3d_t result;
result.x = polar_pos->length *
sin_deg(polar_pos->theta) * cos_deg(polar_pos->phi);
result.y = polar_pos->length *
sin_deg(polar_pos->theta) * sin_deg(polar_pos->phi);
result.z = polar_pos->length * cos_deg(polar_pos->theta);
return result;
}
polar_pos_3d_t convert_to_3d_polar_pos(
const cartesian_pos_3d_t* cartesian_pos) {
polar_pos_3d_t result;
result.length = sqrt(cartesian_pos->x * cartesian_pos->x +
cartesian_pos->y * cartesian_pos->y +
cartesian_pos->z * cartesian_pos->z);
result.theta =
to_degree(acos(cartesian_pos->z / result.length));
result.phi =
to_degree(atan(cartesian_pos->y / cartesian_pos->x));
return result;
}
Code Box 3-6 [ExtremeC_examples_chapter3_2_3d.c]: The source file containing the definitions of the 3D functions
Now we'll create the static library file. To do this, firstly we need to compile the preceding sources to their corresponding relocatable object files. You need to note that we cannot link these object files to create an executable file as there is no main
function in any of the preceding source files. Therefore, we can either keep them as relocatable object files or archive them to form a static library. We have another option to create a shared object file out of them, but we'll wait until the next section to look at this.
In this section, we have chosen to archive them in order to create a static library file. The following commands will do the compilation on a Linux system:
$ gcc -c ExtremeC_examples_chapter3_2_trigon.c -o trigon.o
$ gcc -c ExtremeC_examples_chapter3_2_2d.c -o 2d.o
$ gcc -c ExtremeC_examples_chapter3_2_3d.c -o 3d.o
$
Shell Box 3-10: Compiling source files to their corresponding relocatable object files
When it comes to archiving these object files into a static library file, we need to run the following command:
$ ar crs libgeometry.a trigon.o 2d.o 3d.o
$ mkdir -p /opt/geometry
$ mv libgeometry.a /opt/geometry
$
Shell Box 3-11: Creating the static library file out of the relocatable object files
As we can see, the file libgeometry.a
has been created. As you see, we have moved the library file to the /opt/geometry
directory to be easily locatable by any other program. Again, using the ar
command, and via passing the t
option, we can see the content of the archive file:
$ ar t /opt/geometry/libgeometry.a
trigon.o
2d.o
3d.o
$
Shell Box 3-12: Listing the content of the static library file
As is clear from the preceding shell box, the static library file contains three relocatable object files as we intended. The next step is to use the static library file.
Now that we have created a static library for our geometry example, example 3.2, we are going to use it in a new application. When using a C library, we need to have access to the declarations that are exposed by the library together with its static library file. The declarations are considered as the public interface of the library, or more commonly, the API of the library.
We need declarations in the compile stage, when the compiler needs to know about the existence of types, function signatures, and so on. Header files serve this purpose. Other details such as type sizes and function addresses are needed at later stages; linking and loading.
As we said before, we usually find a C API (an API exposed by a C library) as a group of header files. Therefore, the header file from example 3.2, and the created static library file libgeometry.a
, are enough for us to write a new program that uses our geometry library.
When it comes to using the static library, we need to write a new source file that includes the library's API and make use of its functions. We write the new code as a new example, example 3.3. The following code is the source that we have written for example 3.3:
#include <stdio.h>
#include "ExtremeC_examples_chapter3_2_geometry.h"
int main(int argc, char** argv) {
cartesian_pos_2d_t cartesian_pos;
cartesian_pos.x = 100;
cartesian_pos.y = 200;
polar_pos_2d_t polar_pos =
convert_to_2d_polar_pos(&cartesian_pos);
printf("Polar Position: Length: %f, Theta: %f (deg)\n",
polar_pos.length, polar_pos.theta);
return 0;
}
Code Box 3-7 [ExtremeC_examples_chapter3_3.c]: The main function testing some of the geometry functions
As you can see, example 3.3 has included the header file from example 3.2. It has done this because it needs the declarations of the functions that it is going to use.
We now need to compile the preceding source file to create its corresponding relocatable object file in a Linux system:
$ gcc -c ExtremeC_examples_chapter3_3.c -o main.o
$
Shell Box 3-13: Compiling example 3.3
After we have done that, we need to link it with the static library that we created for example 3.2. In this case, we assume that the file libgeometry.a
is located in the /opt/geometry
directory, as we had in Shell Box 3-11. The following command will complete the build by performing the linking step and creating the executable object file, ex3_3.out:
$ gcc main.o -L/opt/geometry -lgeometry -lm -o ex3_3.out
$
Shell Box 3-14: Linking with the static library created as part of example 3.2
To explain the preceding command, we are going to explain each passing option separately:
-L/opt/geometry
tellsgcc
to consider the directory/opt/geometry
as one of the various locations in which static and shared libraries could be found. There are well-known paths like/usr/lib
or/usr/local/lib
in which the linker searches for library files by default. If you do not specify the-L
option, the linker only searches its default paths.-lgeometry
tellsgcc
to look for the filelibgeometry.a
orlibgeometry.so
. A file ending with.so
is a shared object file, which we explain in the next section. Note the convention used. If you pass the option-lxyz
for instance, the linker will search for the filelibxyz.a
orlibxyz.so
in the default and specified directories. If the file is not found, the linker stops and generates an error.-lm
tellsgcc
to look for another library namedlibm.a
orlibm.so
. This library keeps the definitions of mathematical functions in glibc. We need it for thecos
,sin
, andacos
functions. Note that we are building example 3.3 on a Linux machine, which uses glibc as its default C library's implementation. In macOS and possibly some other Unix-like systems, you don't need to specify this option.-o ex3_3.out
tellsgcc
that the output executable file should be namedex3_3.out
.
After running the preceding command, if everything goes smoothly, you will have an executable binary file that contains all the relocatable object files found in the static library libgeometry.a
plus main.o
.
Note that there will not be any dependency on the existence of the static library file after linking, as everything is embedded inside the executable file itself. In other words, the final executable file can be run on its own without needing the static library to be present.
However, executable files produced from the linkage of many static libraries usually have huge sizes. The more static libraries and the more relocatable object files inside them, the bigger the size of the final executable. Sometimes it can go up to several hundred megabytes or even a few gigabytes.
It is a trade-off between the size of the binary and the dependencies it might have. You can have a smaller binary, but by using shared libraries. It means that the final binary is not complete and cannot be run if the external shared libraries do not exist or cannot be found. We talk more about this in the upcoming sections.
In this section, we described what static libraries are and how they should be created and used. We also demonstrated how another program can use the exposed API and get linked to an existing static library. In the following section, we are going to talk about dynamic libraries and how to produce a shared object file (dynamic library) from sources in example 3.2, instead of using a static library.