Creating and Using Shared-object Libraries on Linux

Russell Bateman
January 2010
last update:

This article explains how to create and consume a small shared-object library on Linux. It also demonstrates a makefile (and doesn't demonstrate using autotools to create it—that's a different and very complex topic in itself).

This isn't an overly complicated thing to do, however, most of what's written about it that you can find via Google seems either to go beyond the mark or stop well short of it. If you're faced today with needing to throw a shared object together and haven't had the pleasure before, this document should bail a lot of water out of your boat.

See useful links I stumbled upon while researching for this article.


Usually I multiply words to attempt to explain much, however, this time I'm going to let the code (mostly the makefile) speak for me.

This isn't the best makefile possible, but it illustrates, as step-by-step as possible:

  1. The C sources depend on headers.
  2. The compiled objects depend on their respective C sources and are set up to contain "position-independent code," hence the -fPIC switch to gcc. Note here that in the case of C++ source, g++ accepts the same option switch.
  3. The shared object depends on the compiled objects and contains extensive and critical command-line switches.
  4. The shared-object target also creates "version" links in imitation of how "professional" shared-object libraries are distributed and consumed.
  5.  
  6. A consuming application, client, is also built by this makefile.
  7.  
  8. A clean-up target is supplied—useful for bouncing the whole process over and over again until it comes out right as well as being good practice.
  9. .PHONY targets are used to keep make from getting too delirious with the less productive targets.
  10. $@ is a built-in make macro that means "target" or "the rule's result." Similarly, $< refers to the "source" or "the thing being built."
  11. Below, find targets, dependencies, executable commands in respective font treatments.

Here is Makefile. The commands for building a) the individual objects and b) the shared object are really the meat of this whole article. Nothing in the rest of it will be of so much interest except to flesh out the context of the discussion.

	.PHONY: clean printstring.c printinteger.c

	libprint.so:     init.o printstring.o printinteger.o
	    ld -shared -soname [email protected] -o [email protected] init.o printstring.o printinteger.o -lc
	    ln -s ./libprint.so.1.0 libprint.so.1
	    ln -s ./libprint.so.1   libprint.so

	init.o:          init.c
	    gcc -fPIC -c $<

	printstring.o:   printstring.c print.h
	    gcc -fPIC -c $<

	printinteger.o:  printinteger.c print.h
	    gcc -fPIC -c $<

	client:
	    gcc -o client client.c -L. -lprint

	clean:
	    rm -f *.so* *.o client

The command I use to build the whole enchilada each time is:

	russ@rhel-32:~/dev/shared-library> make clean ; make ; make client

Supplementary note: I have found that CFLAGS (and CPPFLAGS) including -I, -D definitions, etc., will not be observed by the gcc/g++ command when explicitly coded as is done here. Thus, if I were specifying, for example, additional include paths, I would add $(CFLAGS) into the gcc command line for init.o and the other object targets.


print.h specifies the library's consumable interfaces.

	#ifndef __print_h__
	#define __print_h__

	void printinteger( int );
	void printstring( const char * );

	#endif

printinteger.c is an interface that takes an int argument and prints it to the console.

	#include <stdio.h>
	#include "print.h"

	void printinteger( int integer )
	{
	    printf( "libprint::printinteger(): %d\n", integer );
	}

printstring.c prints a string to the console. It's in a separate file precisely in order to illustrate how to link more than one object into a shared-object library. Some tutorials or examples only showed one file. I had to putter a bit with the ld command before figuring out how to make it work for two files; I kept getting a run-time error complaining that the second library interface didn't exist even though nm seemed to list it as being there.

	#include <stdio.h>
	#include "print.h"

	void printstring( const char *string )
	{
	    printf( "libprint::printstring(): %s\n", string );
	}

init.c contains the shared-object equivalent of Windows DllMain(). It's the start-up and shut-down code for the library. Actually, there are default ones supplied, but I wanted to show using my own and I wanted them to illustrate when they were being called for purely pedagogical reasons.

	#include <stdio.h>

	void _init( void )
	{
	    printf( "libprint::_init()\n" );
	}

	void _fini( void )
	{
	    printf( "libprint::_fini()\n" );
	}

client.c contains the code that consumes the two library interfaces and demonstrates that this whole thing works.

	#include <stdio.h>
	#include "print.h"

	int main( void )
	{
	    printf( "In main()...\n" );
	    printstring( "We're calling the shared library from main()..." );
	    printinteger( 99 );
	    return 0;
	}

Finally, the execution output:

	russ@rhel-32:~/dev/shared-library> ./client
	libprint::_init()
	In main()...
	libprint::printstring(): We're calling the shared library from main()...
	libprint::printinteger(): 99
	libprint::_fini()

Here's a directory listing of what you should find in your development subdirectory when you're done. The library versioning scheme consists of (or so I imagine it does) the formal shared-object being fully named as libprint.so.1.0, meaning "version 1.0 of the print library." A link is set up such that libprint.so.1, or version 1 of the library, points to it. Finally, the basic name of the library object, libprint.so is a link to that version.

	russ@rhel-32:~/dev/shared-library> ll
	total 100
	-rwxrwxr-x 1 russ russ 5159 Jan 29 14:26 client
	-rw-r--r-- 1 russ russ  190 Jan 29 13:55 client.c
	-rw-rw-r-- 1 russ russ  388 Jan 29 13:46 init.c
	-rw-rw-r-- 1 russ russ 1236 Jan 29 14:26 init.o
	lrwxrwxrwx 1 russ russ   15 Jan 29 14:26 libprint.so -> ./libprint.so.1
	lrwxrwxrwx 1 russ russ   17 Jan 29 14:26 libprint.so.1 -> ./libprint.so.1.0
	-rwxrwxr-x 1 russ russ 2630 Jan 29 14:26 libprint.so.1.0
	-rw-rw-r-- 1 russ russ  894 Jan 29 14:26 Makefile
	-rw-rw-r-- 1 russ russ  344 Jan 29 13:43 print.h
	-rw-rw-r-- 1 russ russ  383 Jan 29 13:47 printinteger.c
	-rw-rw-r-- 1 russ russ 1168 Jan 29 14:26 printinteger.o
	-rw-rw-r-- 1 russ russ  387 Jan 29 13:47 printstring.c
	-rw-rw-r-- 1 russ russ 1160 Jan 29 14:26 printstring.o

Were I to distribute an updated version of my library, say with some new functionality, I might redo a few things link-wise in my installation procedure...

	...
	lrwxrwxrwx 1 russ russ   15 Jan 29 14:26 libprint.so -> ./libprint.so.1
	lrwxrwxrwx 1 russ russ   17 Jan 29 14:26 libprint.so.1 -> ./libprint.so.1.1
	-rwxrwxr-x 1 russ russ 2630 Jan 29 14:26 libprint.so.1.0
	-rwxrwxr-x 1 russ russ 2790 Feb 28 10:23 libprint.so.1.1
	...

Don't quote me on this, but I imagine that if I wrote an application that depended crucially on a bug in the first version of my library, I could relink it to use libprint.so.1.0. However, in the usual situation, any application would like to consume the latest version as indicated by simply libprint.so which I would continually update with new releases as time went on.

What's left?

What I haven't covered here is installation in a likely, Linux-ish place like the big boys. You might find a better explanation on versioning than the one I gave above by reading through one or more of the articles to which links are provided below. It transcends my immediate purpose to look into this for now.


Appendix: Some useful links