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”.