Download from
http://www.nyangau.org/dll/dll.zip
.
Many operating systems provide a form of dynamic linking or shared libraries.
There are two basic modes of operation :-
The first case isn't particularly interesting, as far as this document is concerned. All it really provides is a way for applications to share executable code and data. The second case is much more interesting as it allows for peices of programs to be supplied and/or developed seperately and later glued together. From here on onwards, we shall only discuss the latter case.
As an application developer, if you enable your application so that it can load a user specified DLL and call specific entrypoints within it, you allow your clients to extend the capabilities of your program to their needs.
Andys Binary Folding Editor, or BE for short, is a good example of an application which can have its power significantly enhanced by the user providing their own DLL(s). BE is an editor which normally edits files, but by providing a DLL, BE can edit data in any address space the user chooses. This can include the memory space of an embedded processor, perhaps via a JTAG debug connector, effectively turning BE into the data navigation half of a debugger!
As a subsystem developer, if you package up your subsystem so that it is a DLL (or a regular statically linked library), you enable other programs to take advantage of your code.
The manner in which dynamic linking / shared-libraries are supported in each operating system varys wildly. So it can be quite difficult to write platform independant code which supports dynamic linking.
The set of routines in explained in this document allow you to write
DLLs and applications in a largely platform independant way.
The mechanics of how DLLs are implemented on each platform is concealed
from the programmer.
However, as application developers may want other parties to write DLLs
which can be loaded by them (on assorted platforms), sufficient information
on how to do it is given in this document.
Sample makefile
s are included.
How are DLLs implemented on the various supported platforms :-
PATH
environment variable for a file called
DLLNAME.DLL
.
The Microsoft C 6.00 C compiler can be used to compile DLLs and
programs which load them.
Only the Large memory model is supported.
LoadLibrary
and GetProcAddress
routines.
The DLL is found by looking in the current directory, and along the
PATH
environment variable for a file called
DLLNAME.DLL
.
The Watcom C/C++ (version 10.6 or later) and CauseWay DOS extender
(version 1.3 or later) can be used to compile DLLs and programs
which load them.
Despite using a Watcom compiler, I personally still use the Microsoft /
IBM NMAKE (rather than WMAKE), as I prefer it.
DosLoadModule
and
DosGetProcAddr
APIs.
OS/2 looks along the LIBPATH
special environment variable
for DLLNAME.DLL
.
You may need to add the current directory to the LIBPATH
,
or ensure your DLLs are in a directory on the LIBPATH
.
The Microsoft C 6.00 C compiler can be used to compile DLLs and programs
which use them.
These programs and DLLs should be compiled multi-threaded, in accordance
with the sample makefile
supplied here.
DosLoadModule
and
DosQueryProcAddr
APIs.
OS/2 looks along the LIBPATH
special environment variable
for DLLNAME.DLL
.
Pretty much every 32 bit OS/2 system I've seen has had the current
directory in the LIBPATH
.
DLLNAME should be no more than 8 characters long.
The IBM VisualAge C++ compiler (version 3.0, with fixpaks CTC306,
CTD302 and CTU304, or later recommended) can be used to compile DLLs and
programs which load them.
DLLs and programs which use them should be compiled multi-threaded.
LoadLibrary
and
GetProcAddress
APIs.
The MS Visual C++ compiler (version 4.2 or 5.0, or later) can be used
to compile DLLs and programs which load them.
DLLs and programs which use them should be compiled multi-threaded.
load
and unload
.
However, when using C++, one must use the loadAndInit
and
terminateAndUnload
equivelents, to ensure static constructors
and destructors are called, and unfortunately these routines are a
part of the xlC compiler product, and so you can't use them if you
are using the gcc/g++
compilers.
Such shared libraries are named dllname
.
These calls are all you get in AIX 4.1.5 and before.
On AIX 4.2.1 and later, standard support via dlopen
is
provided, but the instant you use it, you render your program unusable
on earlier versions of AIX.
When using dlopen
, the shared libraries are named
dllname.so
and are found by looking along the
LIBPATH
environment variable.
dlopen
can be used from xlc/xlC
or from
gcc/g++
.
In this code, we favor the use of dlopen
, but include
legacy support for load
style operation.
One day, I'll drop support for AIX 4.1.5 and earlier, and when that day
comes, I'll delete the load
stuff from here.
dlopen
family of routines are used.
The library should be named dllname.so
.
Linux searches along the LD_LIBRARY_PATH
environment
variable, in directories listed in the /etc/ld.so.cache
file,
and in the /usr/lib
and /lib
directories.
Rather irritatingly, the current directory isn't automatically searched,
and it isn't in most peoples LD_LIBRARY_PATH
by default.
Remember to set this up.
The gcc C compiler (or g++ C++ compiler) (version 2.7.2.3 or later)
can be used to compile DLLs or programs which load them.
shl_load
family of routines are used.
The library should be named dllname.sl
.
HP-UX searches along the SHLIB_PATH
environment variable.
The standard cc C compiler (or aCC C++ compiler) can be used to
compile DLLs and programs which load them.
dlopen
family of routines are used.
The library should be named dllname.so
.
SunOS searches along the LD_LIBRARY_PATH
environment
variable (and may look elsewhere).
Rather irritatingly, the current directory isn't automatically searched,
and it isn't in most peoples LD_LIBRARY_PATH
by default.
Remember to set this up.
The cc C compiler (or CC C++ compiler) can be used to compile DLLs or
programs which load them.
.dll
extension.
.ndl
extension.
I reserve the right to switch-over-to or preferrably add-support-for the
dlopen
style of dynamic linking on those UNIXes which don't
currently support it widely enough, when they do.
Although this document lists one suitable compiler for each platform, other
compilers may work too (with suitably changed makefile
s).
I list the combinations I use, and therefore know to work.
When implementing a DLL, you are obliged to include the DLL implementors header :-
#include "dllimpl.h"
This essentially defines the calling conventions of the entrypoints to the DLL. These vary somewhat from operating system to operating system. Clearly it is possible to have DLL entrypoints whose calling conventions vary from those dictated by the DLL code documented here. However, deviate far from this document, and things may not work.
Entrypoints should be declared according to the following template :-
DLLEXPORT return_type DLLENTRY function(args) { ... implementation }
On 32 bit DOS (via CauseWay DOS extender), you are obliged to have a dummy main function in your code :-
#ifdef DOS32 int main(int term) { term=term; return 0; } #endif
Unfortunately, 16 bit DOS and AIX don't support dynamic linking well, and so you are also obliged to include a table of entrypoints, in the following form :-
#if defined(xxDOS) || defined(AIX_LOAD) DLL_EXPORT * __start(void) { static DLL_EXPORT exports[] = { (DLL_EP) function1, "function1", (DLL_EP) function2, "function2", ... more 0 , 0 , }; return exports; } #endif
On NetWare, you need to have the following (assuming the DLL is
called libname
) :-
#ifdef NW extern void _dll_term(void); DLLEXPORT void DLLENTRY _libname_term(void) { _dll_term(); } #endif
When writing a program which dynamically loads and uses a DLL, you are obliged to include the DLL users header :-
#include "dlluser.h"
As well as providing access to the calling convention information, this provides calls to load and unload a library of choice, and also to find an entrypoint of a given name within it :-
DLL *dll_load(const char *name); void dll_unload(DLL *dll); DLL_EP dll_entrypoint(DLL *dll, const char *name, const char *args);
A program to dynamically load and use a DLL can be as simple as :-
#include <stdio.h> #include "dlluser.h" int main(int argc, char *argv[]) { DLL *dll; DLL_EP ep; int (DLL_EP_PTR binop_func)(int, int); int result; if ( argc < 3 ) return 1; /* Insufficient command line arguments */ if ( (dll = dll_load(argv[1])) == NULL ) return 2; /* Cannot load named DLL */ if ( (ep = dll_entrypoint(dll, argv[2], "8")) == NULL ) { dll_unload(dll); return 3; } /* DLL does not contain named entrypoint */ binop_func = (int (DLL_EP_PTR)(int, int)) ep; result = (*binop_func)(2,3); printf("%s(2,3)=%d\n", argv[2], result); dll_unload(dll); return 0; }
The example program is fed the name of the DLL and the entrypoint. It is assumed the entrypoint is a function which takes two numbers, does something to them (perhaps it adds them, multiplies them, whatever), and returns a result.
Note the need to do the dll_unload
along the
'DLL does not contain named entrypoint' error path.
The operating system will not always clean up properly (ie: 16 bit DOS).
The 3rd argument to dll_entrypoint
is the number of bytes
the entrypoint has as its parameters.
This is an artifact of the Windows NT __stdcall
calling
convention.
Usually this is simply 4 * the number of parameters (as a string).
The value isn't used on any other platforms (yet).
The DLL_EP_PTR
malarky is an attempt to handle the fact that
function entrypoints can have a number of calling conventions on some systems
(typically PC platforms), and the syntax of specifying the calling convention
varies.
On some compilers the calling convention modifier is written before the
*
, and on others, its afterwards.
eg:
int (* multiply)(int, int); /* UNIX */ int (__stdcall * multiply)(int, int); /* Windows, MS Visual C++ */ int (* _System multiply)(int, int); /* OS/2, IBM VisualAge C/C++ */ \_________/ \______ == DLL_EP_PTR
Can a DLL use another DLLs? I guess so. I see no reason why not. I've never tried it.
The sample makefiles build a DLL and a program which loads a DLL.
CFLAGS = -AL -Gs -W3 -FPa -DxxDOS -nologo LFLAGS = /NOE /NOD /EXEPACK /BATCH /NOLOGO all: libname.dll progname.exe libname.dll: libname.obj dllimpl.lib link $(LFLAGS) /STACK:4096 @<< libname $@ nul dllimpl llibcar; << libname.obj: libname.c dllimpl.h cl -c $(CFLAGS) -Aw $*.c progname.exe: progname.obj dlluser.lib link $(LFLAGS) /STACK:4096 @<< progname $@ nul dlluser llibcar; << progname.obj: progname.c dlluser.h dllimpl.h cl -c $(CFLAGS) $*.c
You may notice the need to link to dllimpl.lib
.
This library provides main
and includes the logic to turn the
executable which is output from the link step, into a TSR, capable of being
invoked by the dlluser.lib
calling code.
Note also, that the 16 bit DOS example uses the alternate floating point support. Floating point support does not work properly in the DLL (aka TSR) environment, because part of the process of terminating and staying resident is to terminate the floating point support in the C run time.
Finally, note the -Aw
DS!=SS switch in the DLL compile.
CW = directory where CauseWay files are CFLAGS = -bt=DOS -dDOS32 -oit -4r -s -w3 -zp4 -mf -zq -fr -zq all: libname.dll progname.exe libname.dll: libname.obj wlink @<< System CWDLLR Name $@ File libname.obj Option Quiet << libname.obj: libname.c dllimpl.h wcc386 $(CFLAGS) -bd $*.c progname.exe: progname.obj dlluser.lib wlink @<< System CauseWay Name $@ File progname.obj Library dlluser.lib LibFile $(CW)\dllfunc.obj,$(CW)\cwapi.obj Option Quiet << progname.obj: progname.c dlluser.h dllimpl.h wcc386 $(CFLAGS) $*.c
Remember, this is a NMAKE file, its probably not suitable for WMAKE.
CFLAGS = -c -Gs -W3 -DO16 -nologo LFLAGS = /NOD /NOI /EXEPACK /BATCH /NOLOGO all: libname.dll progname.exe libname.dll: libname.obj libname.def link $(LFLAGS) @<< libname $@ nul llibcdll os2 libname; << libname.obj: libname.c dllimpl.h cl $(CFLAGS) -ML $*.c progname.exe: progname.obj dlluser.lib link $(LFLAGS) /STACK:4096 /PMTYPE:VIO @<< progname $@ nul dlluser llibcmt os2; << progname.obj: progname.c dlluser.h cl $(CFLAGS) -MT $*.c
We also need a module definition file libname.def
:-
LIBRARY LIBNAME INITINSTANCE PROTMODE DATA MULTIPLE NONSHARED EXPORTS ENTRYPOINT1 ENTRYPOINT2
When compiling 16 bit DLLs using the C run time, there are two choices :-
First, have the program and the DLLs use a single common C run time in a
DLL, which is typically called CRTLIB.DLL
.
This is problematical, especially when many programs exist in the system
which do this, all of which wanting their version of the C run time.
The second approach - the one recommended here - is to have the DLL link
with LLIBCDLL.LIB
, which is a C run time for binding into a DLL.
This has the side effect of turning the code multi-threaded, as this library
is designed for multi-threaded code.
It also has the side effect of making file sizes bigger, but thats not
really an issue nowadays.
In the above makefile
, the calling program links with
LLIBCMT.LIB
as it is multi-threaded too.
Just because a program can be multi-threaded, and is linked as such, it
doesn't mean that it has to use any extra threads.
CWARNS = /W3 /Wcmp+cnd+dcl+ord+par+use+ CFLAGS = /DOS2 /G4 /Gd-m+ $(CWARNS) /Q+ /Ft- LFLAGS = /NOI /NOLOGO all: libname.dll progname.exe libname.dll: libname.obj libname.def ilink $(LFLAGS) /OUT:$@ $** libname.obj: libname.c dllimpl.h icc /C+ $(CFLAGS) /Ge- $*.c progname.exe: progname.obj ilink $(LFLAGS) /PMTYPE:VIO /BASE:0x10000 /EXEPACK /OUT:$@ $** progname.obj: progname.c dlluser.h dllimpl.h icc /C+ $(CFLAGS) /Ge+ $*.c
We also need a module definition file libname.def
:-
LIBRARY libname INITINSTANCE TERMINSTANCE DATA MULTIPLE NONSHARED READWRITE CODE PRELOAD EXECUTEREAD EXPORTS entrypoint1 entrypoint2
CFLAGS = /DWIN32 /G4 /Gs /Oit /MT /nologo /W3 /WX LFLAGS = /NOLOGO /BATCH /INCREMENTAL:NO all: libname.dll progname.exe libname.dll: libname.obj link $(LFLAGS) /DLL $** /OUT:$@ libname.obj: libname.c dllimpl.h cl /c $(CFLAGS) $*.c progname.exe: progname.obj link $(LFLAGS) $** /OUT:$@ progname.obj: progname.c dlluser.h dllimpl.h cl /c $(CFLAGS) $*.c
CFLAGS = -DUNIX -DAIX -DAIX_DLOPEN all: libname.so progname libname.so: libname.o gcc -shared -Wl,-bE:libname.exp chmod a-x $@ libname.o: libname.c dllimpl.h gcc -c $(CFLAGS) -fPIC $*.c progname: progname.o gcc -o $@ progname.o -ldl progname.o: progname.c dlluser.h dllimpl.h gcc -c $(CFLAGS) $*.c
This is a makefile
suitable for use on AIX 4.2.1 or later.
The shared library is created dlopen
style, and the program only
handles such shared libraries.
CFLAGS = -DUNIX -DLINUX all: libname.so progname libname.so: libname.o gcc -shared -o $@ libname.o chmod a-x $@ libname.o: libname.c dllimpl.h gcc -c $(CFLAGS) -fPIC $*.c progname: progname.o gcc -o $@ progname.o -ldl progname.o: progname.c dlluser.h dllimpl.h gcc -c $(CFLAGS) $*.c
-fPIC
says generate position independant code.
-shared
says link as a shared library.
CFLAGS = -DUNIX -DHP all: libname.sl progname libname.sl: libname.o cc -b -o $@ libname.o libname.o: libname.c dllimpl.h cc -c $(CFLAGS) +z $*.c progname: progname.o cc -o $@ -Wl,+s progname.o -ldld progname.o: progname.c dlluser.h dllimpl.h cc -c $(CFLAGS) $*.c
+z
says compile small position independant code.
-Wl,+s
enables shared library loading to look along the
SHLIB_PATH
environment variable.
Using the GNU toolchain :-
CFLAGS = -DUNIX -DSUN all: libname.so progname libname.so: libname.o gcc -shared -o $@ libname.o libname.o: libname.c dllimpl.h gcc -c $(CFLAGS) -shared $*.c progname: progname.o gcc -o $@ progname.o progname.o: progname.c dlluser.h dllimpl.h gcc -c $(CFLAGS) $*.c
Or if using the standard (Forte) Toolchain :-
CFLAGS = -DUNIX -DSUN all: libname.so progname libname.so: libname.o cc -G -Kpic -o $@ libname.o libname.o: libname.c dllimpl.h cc -c $(CFLAGS) -Kpic $*.c progname: progname.o cc -o $@ progname.o -ldl progname.o: progname.c dlluser.h dllimpl.h cc -c $(CFLAGS) $*.c
-fPIC
says generate position independant code.
-shared
says link as a shared library.
CFLAGS = -DUNIX -DCYGWIN all: libname.so progname libname.dll: libname.o gcc -shared -o $@ libname.o libname.o: libname.c dllimpl.h gcc -c $(CFLAGS) -shared $*.c progname: progname.o gcc -o $@ progname.o progname.o: progname.c dlluser.h dllimpl.h gcc -c $(CFLAGS) $*.c
# # Dynamic Link Library # NetWare # Watcom C/C++ 10.6 # CFLAGS = /s /fpi87 /mf /zfp /zgp /zl /zq \ /4s /fpd /wx /bt=NETWARE /DNW .c.obj: wcc386 $(CFLAGS) $*.c all: libname.ndl progname.nlm # Sample DLL libname.ndl: libname.obj dllimpl.obj wlink @<< Format Novell NLM '$@' Name $@ Option Quiet Option Map Option ScreenName 'System Console' Option ThreadName '$@' Debug Novell Module clib, mathlib File libname.obj Library dllimpl.lib Library $(WATCOM)\lib386\math387s.lib Library $(WATCOM)\lib386\noemu387.lib Library $(WATCOM)\lib386\netware\clib3s.lib Import @$(WATCOM)\novi\clib.imp Export _libname_term Export entrypoint1, entrypoint2 << libname.obj: libname.c dllimpl.h # Sample program using a DLL progname.nlm: progname.obj dlluser.lib wlink @<< Format Novell NLM '$@' Name $@ Option Quiet Option Map Option ScreenName '$@' Option ThreadName '$@' Option Stack = 0x1c000 Debug Novell Module clib, mathlib File progname.obj Library dlluser.lib Library $(WATCOM)\lib386\math387s.lib Library $(WATCOM)\lib386\noemu387.lib Library $(WATCOM)\lib386\netware\clib3s.lib Import @$(WATCOM)\novi\clib.imp << progname.obj: progname.c dlluser.h dllimpl.h
The template code above assumes the use of C (rather than C++). On those platforms that allow it, C++ will work, providing care is taken to avoid (or cope with) the name mangling.
Another reason to avoid C++ interfaces is that the name mangling applied varies from compiler to compiler.
Obviously, there would need to be changes to the makefile
s to
invoke C++ compilers and linkers.
On 32 bit DOS, when compiling a C++ DLL, the following line must be added
to the wlink
script :-
Library %watcom%\lib386\plib3r.lib
If you don't explicitly reference plib3r.lib
and the C++ code
uses operator new
then its multithreaded equivelent gets dragged
in (which causes link problems, as DOS is supposed to be single threaded).
Combinations of C and C++ should be thought through carefully :-
Just in case it isn't obvious :-
$@
means the target of the rule (ie: what is being made).
$*
means 'the first filename, without file extension,
after the colon in the make rule'.
$**
means all the filenames to the right of the colon
(ie: all the files required to make the target).
Doesn't seem to work on UNIX versions of make
.
The sample UNIX makefile
supplied with this DLL package
is suitable for Linux, AIX, HP-UX and SunOS.
However, UNIX makefile
s have no standard convention for
ifdef
s.
The supplied makefile
uses a subset of the GNU make
style of ifdef
s.
So you can just type :-
make HP=1
By default, the makefile
s assume LINUX.
Those systems without GNU make
can use the minimal Make
Pre-Processor (called mpp
) to create a makefile
suitable for them.
This script is available from my home page.
eg:
mpp makefile SUN > makefile.SUN make -f makefile.SUN
Writing DLLs that work, or work identically, on a wide variety of platforms is not always a straightforward task.
The main gotcha is that in some environments, the executable program
and DLLs each have their own 'run time'
(eg: 16 bit DOS, 32 bit DOS, 16 bit OS/2, 32 bit OS/2, Win32).
Typically this run time is the data used by the C and C++ run time libraries.
In other systems, the C run time can be independant from the the program.
There may be a single instance of C run time data for the whole program
(eg: Linux with libc.so
).
So this means you can get into trouble if you design your DLL interfaces
such that a DLL malloc
s and the program free
s it
(or vice-versa).
This can be quite difficult to avoid if you are using C++, with
new
and delete
.
Beware DLL interfaces with objects which can delete
themselves.
Watch for this on 32 bit OS/2 and Win32.
Similarly, file handles opened by an application may not be readable
by code in a DLL (or vice-versa).
This is true on Win32.
So if a DLL is required to do file I/O, then it must either do the
open
, or the application must deliver a file handle, and callback
functions which invoke the applications versions of read
and
write
etc..
Similarly for fopen
level I/O, and also for C++ streams.
In some environments, floating point support in a DLL may not work (16 bit DOS), or may be inefficient (16 bit OS/2).
The fact that all UNIXes don't support dlopen
, means I must
code up various similar but different implementations for each UNIX.
The fact that despite AIX 4.2.1 and later support dlopen
,
earlier versions such as AIX 4.1.5 don't.
Why does the Win32 __stdcall
calling convention need to
mangle its function names?
This DLL module, including its source code and sample programs are public domain. Caveat Emptor.