Reusable software is created with points of variation, such that the
software can be bound together with another set of components in a
different context. In this document, I explore the different ways that a
function name can be bound to (associated with) its implementation. A
similar binding may ocurr for data, but there tends to be much less
global data ... right?
Dynamic Binding
Dynamic binding is the association of a functions implementation with
its name that happens during the execution of a program at run-time.
Object oriented programmers are usually familiar with the term "late
binding", which is used to describe the association of an operation
name to the run-time address of the operation. C++ virtual functions
and callback operations are a form of "late binding."
Late binding is useful in those parts of a system that must be varied
as the program executes. Late binding is a dynamic form. There also
exist static forms of binding.
Static Binding
Static binding is when a function name is associated with its
implementation before the program is executed during the build process.
One familiar use of static binding is to bind the source code of a
program to a particular instruction set, and thus a particular type of
processor. This kind of static binding is the job of a compiler.
Typically, a C++ compiler transforms the C++ source code into assembly
language source code for a particular processor type, and that assembly
language source code is subsequently translated to a binary
representation by an assembler.
Static Linker Binding
Binding is also done by another common element of the C++ toolchain
known as the linker. The linker builds a single object module from a
list of object modules. Each object module consists of a set of
symbols that it defines, and may also contain a set of symbols which it
needs to have resolved. Each symbol represents the address of a code or
data object in the runtime environment.
The binding provided by the linker allows for exactly one definition of
a given symbol between the set of object files that constitute a
program. For example, there can be only one address for the code that
implements the function foo() . All clients of foo() will execute the
code at that address.
From a binding perspective, there may be any number of object files
which implement a given function (e.g. foo()) , but only one of those
object files may be linked with the final executable program. ( An
attempt to link more than one such object file will result in a
"multiply defined symbol" linker error.)
This form of binding is implemented in the
BENV
primarily by entries in a project's "libdirs.b" file. However, static
linker binding is also perfomed in portions of the "overrides.b" file,
the "firstobjects.b" file, the "extraobjects.b" file, and sometimes
within the "targetos" and "linkfile" specifications.
Static Text Binding
Other forms of binding are those done by the C++ preprocessor. The C++
preprocessor has two mechanisms that are used for binding 1) text
substitution as performed by the definition of macros, and 2) the
include file mechanism.
Macros
In case you don't already know by now, I am of the opinion that
preprocessor macros are basically evil. To be fair, it is not the macro
mechanism itself that is evil, but rather the abuse it has sufferred at
the hands of programmers. Prior to the enum and inline mechanisms, there
were many valid uses of this facility, however, there are few good
excuses to use/misuse macros in today's world.
The
OSCL and
BENV
make little use of the preprocessor macro facility, with the exception
of include file gards to prevent recursive include problems.
Include Paths and Files
The "include" mechanism of the preprocessor, however, is important
because it allows C++ programmers to separate interface and
implementation. Header files are best used to contain interface, type,
and constant definitions, but header files may also contain short
program definitions, that for performance reasons are declared as
"inline."
It is these "inline" definitions that are another type of binding
performed by the preprocessor. In the usual build sequence for C++
source files, the source file is first preprocessed, and then compiled.
These steps are generally done with a single invocation of the
"compiler". One of the most standard command line options to the
compiler is the "
-I
" option, which allows the programmer to
specify include file paths. Within the source code, "
#include
"
directives specify files with relative (rather than absolute) path
names. Theefore, the include file paths supplied on the command line
allow the programmer to vary which set of include files to be used to
compile a given file. This binding I will refer to is include path
binding.
The
BENV makefile variable LIB_INC_ROOTS is
defined in "overrides.b" and used to specify the include paths for this
type of binding. One common use in the
OSCL
is to specify the processor specific inlined methods for operations such
as those that disable and restore the processor interrupt enable.