About Bindings
The bindings in Derelict are primarily dynamic bindings, as opposed to those in the Deimos project which are all static bindings. This page aims to clarify the difference between the two types of bindings. It is important when starting a new project to understand the implications of each and to select the appropriate type in order to avoid unnecessary frustration.
Terminology¶
Before getting to the differences between the two types of bindings, it would help to understand some relevant terminology as used throughout this documentation. This is especially important since the words static and dynamic are commonly used in multiple contexts. Understanding the terminology will help to avoid confusion in online discussions or when asking for help.
- binding -- a direct interface to a library in one language that enables the library to be used in another language. Typically, the binding provides a one-to-one mapping to the original interface. For example, the names of all type and function declarations in the Derelict packages correspond exactly to those declared in the original C libraries, except in cases where the original API uses a variable or type name that is a keyword in D. Bindings are sometimes mistakenly referred to as wrappers.
- wrapper -- a higher-level interface to a library. Wrappers are typically written in the same language as the underlying API, either that of the original library or a binding to that library. For example, a wrapper for OpenGL written in D would be written on top of an OpenGL binding that is also written in D, while a C++ wrapper would be written on top of the C library directly. Wrappers typically provide functionality that makes using the library more convenient. For example, an OpenGL wrapper for D might use classes to represent OpenGL objects such as textures and shaders, encapsulating commonly grouped function calls into single class methods. None of the packages in Derelict are wrappers. They are all bindings. However, D wrappers can be made that use Derelict packages to interface with the C libraries. For example, an OpenGL wrapper can be written on top of DerelictGL3.
- static linking -- occurs right after the compile step when building an exectuable with a statically compiled language such as C, C++ or D. All of the object files generated by the compiler are fed to a linker, which links them together, along with any static libraries that the application may need, in order to generate the final executable. A static library is an archive containing precompiled object files that are all combined into the final executable. Static libraries on Windows tend to have the
.lib
file extension; static libraries on Posix systems tend to have.a
extensions. Since the objects in a static library become a part of the executable, they add to its file size. When a new version of a static library is released with bug fixes and improvements, all applications that use that library must be recompiled against the new version in order to benefit from the changes. - dynamic linking -- as with static linking, this happens just after the compile step when building an executable with a statically compiled language such as C, C++ or D. However, instead of linking with a static library, the linker links with a shared library directly or, on Windows systems, with an import library (which, confusingly, will have the same
.lib
file extension as a static library). Shared libraries are referred to as dynamic link libraries on Windows (usually with the.dll
extension) and as shared objects on most Posix systems (with the.so
extension). On Mac OS X, one can find shared object files, dynamic libraries with the.dylib
extension, as well as Mac-specific frameworks. Though the names and file formats may be different, all shared libraries are fundamentally the same. Unlike static libraries, they are not combined into the final executable at link time. Instead, information for the system loader is inserted into the executable by the linker. The OS can use this information at run time to load the shared library into memory when the application starts. This allows a single library to be shared by multiple applications, which makes executables smaller and allows all applications using the same library to benefit when the library is updated with bug fixes or improvements, without the need to recompile any of them. It also means that the shared library must be available on the end-user's system in a location that resides on the system library search path. - dynamic loading -- this is a means of using shared libraries without a link step after compilation. Instead, the shared libraries are loaded manually by the executable at run time. To be clear, dynamic linking has a link-time dependency when the final executable is generated and the OS will automatically load the relevant shared libraries at run time; dynamic loading has no link-time dependency and the executable can be generated by the linker without the shared libraries being present on the system. In order for dynamic loading to work, the program must make use of OS-specific APIs (or third-party wrappers) to load the shared libraries into the process memory space. Since the linker is not used to generate locations in memory for the shared library's symbols, the program must declare pointers to the symbols in the shared library that it wants to use and manually match them with the symbols in the shared library after loading it into memory. In other words, the shared library is loaded manually rather than automatically. The shared library must also be available on the end-user's system in a location that is on the system library search path, otherwise the system APIs will not be able to find it. Aside from eliminating link-time dependencies, this approach allows different implementations of a shared library's interface to be easily swapped at run time.
Dynamic vs. Static Bindings¶
The primary difference between a dynamic and a static binding is that the former has no link-time dependency on the library to which it binds. Both types require that the bindings themselves be either linked with the executable or compiled along with the application. However, static bindings have the additional requirement that the bound library must be either statically linked (with a static library) or dynamically linked (with a shared library) when the executable is generated. Dynamic bindings make use of dynamic loading to load shared libraries at run time and, therefore, have no link-time dependencies beyond the binding itself. This is explained below.
Static Bindings¶
The source code for static bindings in D, like those found in the Deimos project, tends to look very similar to the C or C++ headers for the C library to which they bind. Functions are declared with no implementation. For example:
extern(C) void SomeFunction(int);
Given a C library, MyLib
, which exposes the function SomeFunction
declared above, the user of the D binding for MyLib
must either statically link with the MyLib
static library (MyLib.lib
on Windows, libMylib.a
elsewhere) or with the MyLib
shared library (the import library MyLib.lib
on Windows, or libMyLib.so
elsewhere). Additionally, the user must also link with the MyLib
binding itself, most often as a static library, if it contains anything more than function and type delcarations or templates, e.g. non-templated helper functions that have implementations.
On Windows, there can be some effort required to link with the C library. At issue is the object file format used by different linkers. For example, when compiling on 32-bit Windows, DMD uses the OPTLINK linker by default, which only understands the OMF object format. This is the format that was commonly used on Windows back in the DOS days. Modern Windows compilers and linkers more typically use COFF instead. As such, most precompiled static and shared libraries available online are in the COFF format. To be used with the default 32-bit DMD toolchain, they must either be converted to OMF using a conversion tool, or the source for the library must be compiled with a compiler that generates OMF files, such as the Digital Mars C & C++ Compiler (DMC). However, when compiling with DMD in 64-bit mode (the -m64
switch) or with the -m32mscoff
switch, DMD requires the Microsoft C and C++ build tools. The Microsoft linker understands the COFF format, so the conversion step is not necessary. Detailing the object file formats used by every D compiler is beyond the scope of this documentation. It's up to the user to know the output of the tools being used.
Different people may see different benefits and drawbacks to using static bindings. One of the biggest benefits, perhaps, is that a static binding offers a choice between statically linking with and dynamically linking with a C library. However, no matter which type of linking is chosen, there is no way to escape the link-time dependency imposed by a static binding.
Dynamic Bindings¶
Not only are dynamic bindings used differently than static bindings, they are implemented differently. Normal function prototypes do not work with dynamic bindings because of the fact that such bindings make use of dynamic loading. All of the functions in the bound library must be declared in the binding as function pointers. The MyLib
example above converted to a dynamic binding format in D might look something like this:
extern(C) alias p_SomeFunction = void function(int); p_SomeFunction SomeFunction;
When an application using the MyLib
dynamic binding is compiled, there is no link-time dependency beyond the binding itself. At run time, the user of the dynamic binding must typically call a function to load the MyLib
shared library (unless the binding is configured to do it automatically, such as via a static constructor in D). The loader will first call into the OS API to load the shared library into the application's memory space. After that, it will again call into the OS API to fetch the address of the SomeFunction
implementation and assign it to the SomeFunction
variable declared above. If successful, then SomeFunction
is ready to use from that point on. In Derelict, dynamic loading is done through a loader in the DerelictUtil library, which is why the library is a dependency of each binding in Derelict.
Aside from eliminating the link-time dependency on the bound library, dynamic bindings give the client control over the case where the shared library fails to load, an event which typically occurs when the library is missing from the end user's system, it's missing some symbols the program expects to find, or is otherwise corrupt. When using a static binding with dynamic linking, the OS itself reports an error in these cases, which may not always be desirable. Because a dynamic binding uses dynamic loading, it is possible for the application to detect such errors and react in a manner appropriate for the application, such as displaying a message box with a link to a tech support page.
Conclusion¶
The information above is intended to shed light on the differences between static and dynamic bindings. Generally, neither type of binding is "better" than the other. For most applications, it probably doesn't matter which type is used as long as the application programmer is comfortable with the concepts of compiling, linking, and loading. The pros and cons of each approach can take on different weights in different contexts, but debating or explaining such is beyond the scope of this documentation.