How to build a C dynamic library, then link programs against it

I'm using gcc on a pretty generic Ubuntu Linux.

If we look at the file types of shared libraries, using the file program, we get something that looks like:

/lib32/libc.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=f1d08a3918f1a38e9f085dfadb39cf8ac2750c6a, for GNU/Linux 3.2.0, stripped

or

/lib/x86_64-linux-gnu/libc.a: current ar archive

or

/lib/x86_64-linux-gnu/libSDL2-2.0.so.0.18.2: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=4d5b3c4d6ed820f4264d19e6b9dee40106d05359, stripped

Static libraries have an .a filename extension, are made with the ar archiver, and are an archive (think tarball, but not compressed and not-tar) of .o files. Shared libraries (or shared objects) have the .so filename extension, and are handled specially by the operating system, being more complicated to load and run.

Creating the library itself

We write a .c file containing the code of the library itself, and a .h file containing a definition of all functions in it.

Source code of our libfoo.c
/* libfoo: a small test library with a couple of mathematical functions.
 * 2023-06-27 */
#include <limits.h>
#include <stdint.h>
#include "libfoo.h"

#define LIBFOO_ABSVAL(x) ((x) < 0 ? -(x) : (x))

long approximate_times_pi(long n) {
	if (LIBFOO_ABSVAL(n) > (LONG_MAX / 355)) {
		if (LIBFOO_ABSVAL(n) < (LONG_MAX / 22)) {
			return (22 * n) / 7;
		} else if (LIBFOO_ABSVAL(n) < (LONG_MAX / 3)) {
			return 3 * n;
		} else {
			return (n < 0) ? LONG_MIN : LONG_MAX;
		}
	} else {
		return (355 * n) / 113;
	}
}

long ackermann_phi(long m, long n, long p) {
	if (m < 0 || n < 0 || p < 0) {
		return 0;
	}
	if (p == 0) {
		return m + n;
	} else if (n == 0) {
		if (p == 1) {
			return 0;
		} else if (p == 2) {
			return 1;
		} else {
			return m;
		}
	} else {
		return ackermann_phi(
			m,
			ackermann_phi(m, n - 1, p),
			p - 1
		);
	}
}

int dragon_a014577(unsigned long long n) {
	if (n % 4 == 0) {
		return 1;
	} else if (n % 2 == 0) {
		return 0;
	} else {
		return dragon_a014577(n >> 1); // relying on (2n+1)>>1 == n
	}
}
Source code of our libfoo.h
/* libfoo: a small test library with a couple of mathematical functions.
 * 2023-06-27 */
#ifndef LIBFOO_H_INCLUDE_GUARD
#define LIBFOO_H_INCLUDE_GUARD

long approximate_times_pi(long n);
long ackermann_phi(long m, long n, long p);
int dragon_a014577(unsigned long long n);

#endif

We then compile the library, using the -shared option given to gcc. Instead of it, or in addition to it, we may use the -fPIC option to explicitly specify that we want position-independent code, suitable for shared library use.

$ gcc -Wall -Wextra -Wpedantic -shared -fPIC -o libfoo.so libfoo.c

We keep the compiled library in a safe place ;)

The -Wall -Wextra -Wpedantic options are all optional, but I like to use them, and will include them from here on. You may omit them if you wish.

We investigate, with file, the file type of our newly-generated shared library:

libfoo.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=ff386f46ab0c939a26076a79f45244bebd615f15, not stripped

Linux distributions typically put shared libraries in /lib/, /usr/lib/ or /usr/share/lib/, or something like that. The library location can be overriden when starting up the program by setting the environment variable LD_LIBRARY_PATH; this way, we can have the library be in the same directory our program runs in.

Creating a program that uses the library

We write a small program that links against the shared library. We use the same .h file so that our program knows what the shared library contains (i.e. we need to know what the functions are called and what their type signatures are).

Source code of our using_libfoo.c
/* A small program that is to be dynamically linked to our libfoo.
 * 2023-06-27 */
#include <stdio.h>
#include "libfoo.h"

int main() {
	long circle_radius = 100;
	long radius_squared = circle_radius * circle_radius;
	long circle_area = approximate_times_pi(radius_squared);
	long square_area = circle_radius * circle_radius * 4;
	float area_covered = (float) circle_area / (float) square_area;
	printf("A circle with a radius of %ld has a surface area of %ld.\n", circle_radius, circle_area);
	printf("The circle covers up %f %% of a square of the same radius.\n", area_covered * 100);
	printf("The %ldth element in OEIS sequence number A014577 is %d.\n", circle_area, dragon_a014577(circle_area));
	return 0;
}

However, we don't compile with the libfoo.c library source file itself. Instead, we use the -llibname option: this instructs the linker (which is run by the compiler, but is technically separate from it) to add instructions for loading the library into our executable file. The operating system's loader, which understands our executable format (ELF on Linux) will use these instructions (and possibly also the LD_LIBRARY_PATH environment variable) to locate and load into memory the libfoo.so shared library we created.

We also use the -Ldirectory parameter to specify what directory the -llibname option will cause libraries to be searched for in. Since we have all our code and binaries in the current working directory, we just pass in -L. for "this directory". You may also use -L$(pwd).

$ gcc -Wall -Wextra -Wpedantic using_libfoo.c -L. -lfoo -o using_libfoo_dynamic

One thing that tripped me up for a while, and you should keep this in mind, is that the -l option assumes that the name of the library starts with "lib" and ends with ".so". Thus, to link against libfoo.so, you must pass it -lfoo, not -llibfoo.so: if you give it the latter, it will give you a "cannot find -llibfoo.so: No such file or directory" error.

When we try running the program we get an error:

$ ./using_libfoo_dynamic
./using_libfoo_dynamic: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

This is because the linker cannot find the shared library, because it doesn't know to check in the current directory by default. Thus, we use the LD_LIBRARY_PATH variable:

$ LD_LIBRARY_PATH=. ./using_libfoo_dynamic
The output of the program
A circle with a radius of 100 has a surface area of 31415.
The circle covers up 78.537498 % of a square of the same radius.
The 31415th element in OEIS sequence number A014577 is 0.

If we were to not specify the library

If we were to not specify the library to link to with the -l option, we would get an error that looks like this:

$ gcc -Wall -Wextra -Wpedantic -o using_libfoo_dynamic using_libfoo.c
/usr/bin/ld: /tmp/ccaQ1uBl.o: in function `main':
using_libfoo.c:(.text+0x28): undefined reference to `approximate_times_pi'
/usr/bin/ld: using_libfoo.c:(.text+0xbb): undefined reference to `dragon_a014577'
collect2: error: ld returned 1 exit status

Static linking instead

To bypass the hassle of dynamic linking and shared libraries (there are tradeoffs, don't immediately dismiss either system), and since we already have the source code to the library, we can just compile them all as one unit:

$ gcc -Wall -Wextra -Wpedantic -o using_libfoo_static libfoo.c using_libfoo.c

Of course, "real programmers" don't keep re-compiling the same C files over and over again. Instead, each file is compiled into its own object file, with an .o extension, and then those files are linked separately, either statically into a single executable or dynamically into a set of dynamic libraries and executables linking to those libraries.

Sources

I used this guide a lot: Creating a shared and static library with the gnu compiler (gcc)

Also the GCC documentation, especially the sections on directory options, linking options, and code generation options.

This post was written on 2023-06-27 and, and last edited on 2023-06-27. Back to the index.