21.2. Shared Library Example

In doc/example/shared-library, you will find an example of using shared libraries. This example contains an executable program and two implementations of the same interface, both provided in shared libraries. In the shared-library/prog directory, you will find a simple program. Here is its Abuild.conf file:

shared-library/prog/Abuild.conf

name: prog
platform-types: native
deps: shared

All it does is depend on the build item shared. This program doesn't have to do anything special in order to link against the shared library. Here is the shared build item's Abuild.conf:

shared-library/shared/Abuild.conf

name: shared
child-dirs: include impl1 impl2
deps: shared.impl1

This is a pass-through build item that depends upon shared.impl1. Here is that build item's Abuild.conf:

shared-library/shared/impl1/Abuild.conf

name: shared.impl1
platform-types: native
deps: shared.include

This build item depends on an item called shared.include. Although, in general, putting your header files in a separate build item is risky (see Chapter 30, Best Practices for a discussion), in this case, we want to do this so that we can have two separate implementations of this interface that reside in two different shared libraries. By making this build item private to the shared build item name scope (see Section 6.3, “Build Item Name Scoping”), we effectively prevent outside build items from depending on it directly.

Here is the first implementation's Abuild.mk file:

shared-library/shared/impl1/Abuild.mk

TARGETS_lib := shared
SRCS_lib_shared := Shared.cc
SHLIB_shared := 1 2 3
RULES := ccxx

What we have here is a normal library Abuild.mk file except that we have set the variable SHLIB_shared to the value 1 2 3. This tells abuild to build the shared library target as a shared library instead of a static library using the version information provided. On Windows, abuild will create shared1.dll along with shared1.exp and shared.lib. On UNIX, it will create libshared.so.1.2.3 and will make libshared.so and libshared.so.1 symbolic links to it. UNIX executables that link with -lshared will need to find libshared.so.1 in their library paths at runtime. Windows executables that link with -lshared will need to find shared1.dll in their executable paths at runtime.

This shared library consists of a single file called Shared.cc. Here is the header file Shared.hh:

shared-library/shared/include/Shared.hh

#ifndef __SHARED_HH__
#define __SHARED_HH__

class Shared
{
  public:
#ifdef _WIN32
    __declspec(dllexport)
#endif
    static void hello();
};

#endif // __SHARED_HH__

This is the implementation of the interface:

shared-library/shared/impl1/Shared.cc

#include <Shared.hh>
#include <iostream>

void
Shared::hello()
{
    std::cout << "This is Shared implementation 1." << std::endl;
}

Notice the __declspec(dllexport) line that is there for Windows only. This is necessary to make Windows export the function to a DLL. No such mechanism is required in a UNIX environment. Our Abuild.interface file looks like a normal Abuild.interface file for libraries except that it omits an INCLUDES variable and declares a special mutex variable:

shared-library/shared/impl1/Abuild.interface

# Declare this "mutex" variable to prevent multiple implementations of
# the "shared" interface from being in a build item's dependency chain
# at the same time.
declare shared_MUTEX boolean

LIBDIRS = $(ABUILD_OUTPUT_DIR)
LIBS = shared

The INCLUDES variable is set in the shared.include build item's Abuild.interface instead:

shared-library/shared/include/Abuild.interface

INCLUDES = .

The mutex variable is a normal interface variable. We declare the same variable in the Abuild.interface file for shared.impl2. Since abuild won't allow any interface variable to declared in more than one place, this effectively prevents any one build item from simultaneously depending on both shared.impl1 and shared.impl2. Please note that we have included the name of the public item, “shared” in the name of the mutex variable “shared_MUTEX“ to avoid namespace collisions with other unrelated build items.

Our second implementation is not in the dependency chain of our program. It resides in the impl2 directory. Here are its Abuild.conf and Abuild.mk:

shared-library/shared/impl2/Abuild.conf

name: shared.impl2
child-dirs: static
platform-types: native
deps: shared.include shared.impl2.static

shared-library/shared/impl2/Abuild.mk

TARGETS_lib := shared
SRCS_lib_shared := Shared.cc
SHLIB_shared := 1 2 4
RULES := ccxx

You will notice in this case that this build item depends on a static library that its private to its own build item name scope. This static library provides additional functions that are used within the shared library. Since the static library is linked into the shared library and is not intended to provide any public interfaces, we want to avoid having the static library appear on the link statement for executables that link with this shared library. To do that, we have to do some extra work in our Abuild.interface file. Here are that file and the after-build file that it loads:

shared-library/shared/impl2/Abuild.interface

# Declare this "mutex" variable to prevent multiple implementations of
# the "shared" interface from being in a build item's dependency chain
# at the same time.
declare shared_MUTEX boolean

LIBDIRS = $(ABUILD_OUTPUT_DIR)
after-build after.interface

shared-library/shared/impl2/after.interface

reset LIBS
LIBS = shared

Notice that we reset the LIBS variable and add our own library to it after the build has completed. This effectively replaces everything that was previously in the LIBS variable with our library for items that depend on us. In this case, the shared.impl2.static build item had added static to LIBS in its Abuild.interface file:

shared-library/shared/impl2/static/Abuild.interface

INCLUDES = .
LIBDIRS = $(ABUILD_OUTPUT_DIR)
LIBS = static

The effect of our reset that the static library added to LIBS there is available to shared.impl2 during its linking but not available to those who depend on shared.impl2. [44]

Finally, we can run our program. Remember that in order to run the program, you must explicitly add the directory containing the shared library whose implementation you want to use to your LD_LIBRARY_PATH on UNIX or your PATH on Windows. If you set this variable to include the output directory for shared.impl1, you will see this output:

shared-library-prog-impl1.out

This is Shared implementation 1.

If you set it to the shared.impl2 build item's directory, you will see this instead:

shared-library-prog-impl2.out

This is Shared implementation 2.
This is a private static library inside implementation 2.

Note that we could have made shared depend on shared.impl2 instead of shared.impl1 and gotten the same results. Hiding the actual shared library implementation behind a pass-through build item provides a useful device for allowing you to reconfigure the system later on, including replacing place-holder shared library-based stub implementations with static library implementations later in the development process. With careful planning, this type of technique could be used to provide a shared-library based stub system that could be swapped out later with very little effect on the overall build system.



[44] What is going on here is a bit subtle. At first, resetting LIBS may seem quite drastic, but it really isn't. The reset statement only resets the state of LIBS as it was at the time that this Abuild.interface file was processed. Any build item that depends on this item will still have all the other items that were added to LIBS by other build items. To really understand how this works, please see Section 33.7, “Implementation of the Abuild Interface System”.