If you use Windows, you can't escape dynamic link libraries (DLLs). Just take a look in your \Windows and \Windows\System directories. I run Windows NT 4, and I count more than 650 DLLs in the \Winnt directories on my system. So, let's not debate the facts. DLLs are part of everyday life in Windows.
The question, then, is whether you need to create and use DLLs. That's what you find out today. Specifically, you will look at the following:
By the end of today you will have a good idea of whether DLLs are for you. I suspect that DLLs will fit into your development plan somewhere down the road.
Simply put, DLLs are very useful. Today I discuss the benefits of DLLs and how this translates for you, the Delphi programmer. First, let's explore exactly what a DLL is.
New Term: A DLL (dynamic link library) is one or more pieces of code stored in a file with a .dll extension.
The DLL code can be called from executable programs, but the DLL itself isn't a stand-alone program. You can consider the DLL a helper file for a main program. Applications that use code in DLLs are called calling applications because they call functions or procedures in the DLL.
There are two types of DLLs: code DLLs and resource DLLs. This is not an exclusive arrangement. You can have both code and resources in the same DLL, and there's no problem with that whatsoever. In some cases, though, it's convenient to store your code in one DLL and your resources in another. I'll talk about why you might want to do that in the section "Using Resources in DLLs."
Code contained in a DLL can be of two primary forms. The first form is a standalone function that you call from your main application. You have already done that several times in this book. Remember the code from Day 14, "Advanced Programming," as follows:
Screen.Cursors[myCursor] := LoadCursor(HInstance, `MYCURSOR');
or this, also from Day 14:
DrawText(Canvas.Handle, Panel.Text, -1, Temp, DT_CENTER or DT_VCENTER or DT_SINGLELINE);
Both LoadCursor and DrawText are Windows API functions. You probably knew that. But did you know that these functions are called from DLLs? When you think about it, these functions have to be stored somewhere. But where? In this case, both LoadCursor and DrawText are contained in the Windows DLL called User32.dll.
And what about all those common dialog boxes you've been using, such as the File Open dialog box, the Print dialog box, and the Printer Setup dialog box? They have to come from somewhere. In this case, these dialog boxes are stored as resources in the file called Comctl32.dll. So, without knowing it, you have been using DLLs all along.
The second form of code contained in a DLL consists of procedures and functions used internally by the DLL. These functions and procedures perform some task in the DLL and are not visible to the outside world (applications using the DLL).
Using DLLs is one thing, but using and creating your own DLLs is an entirely different matter, right? Not really. After you create a DLL, you can call functions and procedures in the DLL just as you do when calling Windows API functions. Creating DLLs is not difficult, so there's nothing to stop you from putting DLLs to work for you.
That's a good question. After all, what does a DLL give you? DLLs offer these benefits:
Let's take a look at these points individually and see whether I can convince you what a good thing DLLs are.
Code reuse is a big part of object-oriented programming. After all, why reinvent the wheel? DLLs aid in code reuse in a big way. Let's say you have a large amount of code you wrote to handle a specific task in Windows. You've worked hard to write that section of code, so it would be nice if you could reuse it in any of your applications that need it. DLLs enable you to do exactly that.
What you do is compile all of your code into a DLL . Then all you have to do in order to use the code in any of your programs is basically load the DLL from an application and begin using the code. What could be easier? (Actually, there are a couple other steps required, but they are simple and I don't want to get ahead of myself by explaining them now.)
Now you have this code at your fingertips any time you need it. Better yet, you can give the DLL to your programming pals, and they can use the code, too. Hey, this is starting to sound good. Better yet, you can sell the DLL to other programmers. You put all that work into it, so you might as well get out of it what you can!
NOTE: You can easily create DLLs that can be called from applications written in C++Builder, Visual Basic, Visual C++, or other programming environments. Of course, this depends on the code contained in the DLL and how that code is called, but it can be done.
So, do you see where this is heading? After writing a DLL, you can use it with ease whenever and wherever you want. Getting at all the goodies contained in your DLL is just a few mouse clicks away.
Sharing code ties in with code reuse but takes it a step farther. Let's say that you are a programmer for a large corporation. You might have hundreds of users, each with his or her own system. (I'll ignore networking considerations for this example.) Let's say that you've written five applications for those users. Further, let's assume that each of those five applications uses a common set of code that compiles to 100KB (a modest figure). If you don't use a DLL, you will have 100KB of code repeated five times (once for each of the five applications) for a total of 500KB of code. That's code waste, folks.
A better approach is to put the classes in a DLL. Each of the five programs can use the same DLL for the common code. That's part of the beauty of DLLs: After the DLL is written, all your applications ca n share it. Each user will get an additional 100KB DLL on his or her machine, but each of the five applications will be reduced in size by 100KB. The bottom line is that each user saves 400KB of disk space. Saving 400KB might not sound like a lot, but if you multiply it by hundreds of users, it starts to add up from a corporate perspective (bean counters tend to look at those types of issues). This is just a simple example. In a real-world situation, you could easily save several megabytes per user by using DLLs.
What if three of the five programs are running simultaneously? No problem. Each program pulls code from the DLL as needed, and there are no conflicts. Windows keeps track of who's calling whom and makes sure that it all works together. All you do is write the DLL and start using it. (If this discussion sounds familiar, it is probably because I used roughly this same argument when discussing runtime packages on Day 8, "Creating Applications in Delphi.")
By keeping your code compartmentalized, you can deal with it more easily when it comes time to update your code. I am not advocating a separate DLL for each aspect of your program, but when and where it is sensible to break up your program into related chunks of code, you should do so. There isn't much benefit in breaking an application into DLLs just for the sake of compartmentalizing the code. If you have libraries of common code that you have written, though, it does make sense to keep those libraries in separate DLLs. Again, this is most obvious when creating libraries that can be used by any of your programs.
One advantage to this approach is that your applications can be more easily upgraded if necessary. Let's face it. Even the best of programs go to market with bugs. It's more a question of how many or how severe than a question of whether there are bugs at all. If you discover bugs in your DLL, you can fix the bug and ship a new DLL to your users rather than recompile and ship an entire executable file.
At first this might not appear to benefit the types of applications you have written so far. However, you might eventually write programs several megabytes in size with dozens of source modules. In that case, compartmentalizing makes much more sense.
A few years ago, you could write a program and not worry about the international market. You would write the program and create menu items, dialog boxes, hint text, error messages, and so forth in your native language, and that was that. You put the program on the market and didn't think about it again.
The world, however, is becoming smaller. With the explosion of the Internet and the World Wide Web, things are completely different than they were just a couple of years ago. You can create a demo of your program or maybe a shareware version, and put it on the Internet. Within hours, minutes even, people all around the world have access to your program. It's exciting and frightening at the same time. This phenomenon means that you must plan ahead and prepare for translating your program into other languages.
One way you can do this is to create resource DLLs that contain string resources in various languages. You can have a separate DLL for each language, or as an alternative, you can have all the string resources in a single DLL and load the correct language version of each string at runtime. At runtime, you can use the Windows API function LoadString to load the strings and assign them to various components, as needed.
NOTE: One disadvantage to the Delphi programming model is that it doesn't use traditional resources as other programming environments do. Resources such as menus are not loaded as resources but rather are contained within the form's resource. This makes internationalization more difficult and is one of the few times that this model works against you.
Again, planning ahe ad can save you a lot of work later. If you plan your application with internationalization in mind, it is much easier to translate your application into other languages when necessary.
Today's systems are faster, have more RAM, and have more hard disk space than ever before. It's easy to fall into the mode of "Hey, I'm only using 2MB of system RAM, so what's the big deal?" The truth is, you should always be conscious of how much of your users' system resources you are consuming. Here again, DLLs can help. This is really a continuation of the discussion on sharing code among different applications.
Let's go back to the example I used earlier. (Remember, you have five applications using some common code.) If you don't use DLLs, when several of your programs are running at the same time, they will all load some of the same code into memory. You are wasting system resources because each program loads its own copy of the exact same code. Rather than have each program load the same code, you use a DLL and load the code once. All your applications can use the same code in memory and reduce the drain on the system.
As with any other Pascal unit, a DLL unit follows a particular format. Listing 19.1 shows a minimal DLL unit.
library TestDLL; uses SysUtils, Classes, Forms, Windows; procedure SayHello(AForm : TForm); begin MessageBox(AForm.Handle, `Hello From a DLL!', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end; exports SayHello; begin end.
First notice the library keyword at the top of the unit. The library keyword identifies this unit as a DLL unit. This DLL has a single procedure called SayHello. The SayHello procedure is no different than any other Object Pascal procedure.
Now turn your attention to the exports keyword near the bottom of the unit. Any procedure or function identifiers in the exports section are exported from the DLL. In this case the SayHello procedure is exported. Exporting functions and procedures is discussed in detail in the section "The exports Keyword."
Finally, at the end of the DLL unit you see begin and end keywords. This code block is the DLL's main code block and is where you put any code that your DLL needs to execute when the DLL initially loads. In many cases (as in this example), you don't need any initialization code at all, so this code block is empty.
Writing a DLL is not difficult. There are a couple of points to be aware of, but most of it is straight Object Pascal programming. Let's start with a discussion on the basics of writing DLLs. After that, you can build your first DLL.
Functions and procedures in a DLL fall into two basic categories:
A DLL might also contain classes that could, of course, have methods. I'm not going to talk about methods of a class contained in a DLL at this time, but I'll discuss the other two types of functions and procedures next.
Functions and procedures called within a DLL require no special handling. You declare this type of function or procedure just as you do any function or procedure. The function or procedure can be called by other functions within the DLL, but it can't be called from outside the DLL. In other words, a calling application will not have access to these functions and procedures. They can be considered private to the DLL, much as private methods of a class are private to the class in which they reside. In effect, a calling application won't be able to "see" the functions and procedures to even know they exist.
NOTE: In addition to functions and procedures, a DLL can contain global data that all procedures in the DLL can access. In 16-bit Windows, global data in a DLL is shared among all instances of the DLL. In other words, if one program changed the global variable x to 100, x would have the value 100 for all other applications using the DLL as well. In 32-bit Windows, this is not the case. In 32-bit Windows, a separate copy of a DLL's global data is created for each process that attaches to the DLL.
Another category of functions and procedures comprises those that can be called from outside the DLL. These are functions and procedures that are made public by exporting them from the DLL. They can be called by other functions and procedures in the DLL or by applications outside the DLL.
NOTE: Functions and procedures in a DLL can be called by executable applications and also by other DLLs. In other words, one DLL can call functions and procedures in another DLL.
After a function or procedure has been exported, you call it from your application.
To export a function or procedure, you use the exports keyword in the DLL. Refer to Listing 19.1 for an example of a DLL that exports a procedure called SayHello. Because the SayHello procedure is exported, it can be called from any Delphi application.
The most common way of exporting functions and procedures is by name--for example,
exports SayHello, DoSomething, DoSomethingReallyCool;
These procedures are exported by their identifier name. You might have noticed that the exports section has the same syntax as a uses list. Each identifier to be exported is listed, separated by a comma. A semicolon follows the last identifier in the list.
You can also export procedures and functions by ordinal value. Procedures and functions are exported by ordinal value by implementing the index keyword like this:
exports SayHello index 1, DoSomething index 2, DoSomethingReallyCool index 3;
When you import the function in the calling application, you specify the ordinal number (I'll discuss importing functions and procedures later in the section, "Calling Using Static Loading"). Most of the time you will export functions and procedures by name and won't bother with exporting by ordinal.
NOTE: Delphi automatically assigns an ordinal value to every exported function and procedure regardless of whether you specify an index number. Specifying an index number lets you control the ordinal value of the exported function or procedure.
Exporting a function or procedure is only half the story. When you build the application that calls the function or procedure, you must import the functions and procedures you want to call from the DLL. I'll talk about importing functions and procedures later in the section "Calling Functions and Procedures in DLLs."
NOTE: The global variable HInstance, when used in a DLL, will contain the instance handle of the DLL.
To find out whether your code is executing in a DLL or in an application, check the value of the global variable IsLibrary. IsLibrary is True when called from a DLL and False when called from an application.
TIP: If you are having trouble exporting functions or procedures, run the TDUMP utility on the DLL. TDUMP produces information containing a section on symbols exported from the DLL. Examining that section will give you a better idea of where the problem lies. To see just the exported symbols, run TDUMP with the --ee switch--for example,tdump --ee mydll.dll
Remember, you can redirect the output of a command-line application to a text file with the > symbol:
tdump --ee mydll.dll > dump.txt
As I mentioned earlier, any initialization code that the DLL needs to execute can be done in the DLL's main code block. That's easy enough, but what about finalization code? DLLs don't have initialization and finalization sections as other units do. You might dynamically allocate memory in the DLL's main code block, but where do you free it? The answer is a DLLProc. A DLLProc is a procedure that is called at certain times during the life of a DLL. I'll explain how to use a DLLProc in just a bit, but first let me tell you why DLLProc exists.
A DLL receives messages from Windows when it is loaded into memory and just before it is unloaded from memory. It also receives messages when a process attaches to or detaches from an already-loaded DLL (as in the case of several applications all using the same DLL). To get these messages, you create a procedure with a specific signature and assign the address of that procedure to the global variable DLLProc. A typical DLLProc procedure might look like this:
procedure MyDLLProc(Reason: Integer); begin if Reason = DLL_PROCESS_DETACH then { DLL is unloading. Cleanup code here. } end;
Just declaring a DLLProc procedure is not enough to ensure that the DLLProc will be used. You must also assign the address of the procedure to the DLLProc global variable. You do that in the DLL's main code block. For example:
begin DLLProc := @MyDLLProc; { More initialization code. } end.
This code will be executed as soon as the DLL loads. The DLLProc is now installed and will be called automatically as processes attach and detach from the DLL or when the DLL unloads. Listing 19.2 shows the source code for a DLL that implements a DLLProc.
library TestDLL; uses
SysUtils,
Classes, Forms, Windows; var SomeBuffer : Pointer; procedure MyDLLProc(Reason: Integer); begin if Reason = DLL_PROCESS_DETACH then { DLL is unloading. Cleanup code here. } FreeMem(SomeBuffer); end; procedure SayHello(AForm : TForm); begin MessageBox(AForm.Handle, `Hello From a DLL!', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end; { More DLL code here that uses SomeBuffer. } exports SayHello; begin { Assign our DLLProc to the DLLProc global variable. } DLLProc := @MyDLLProc; SomeBuffer := AllocMem(1024); end.
As you might have surmised from Listing 19.2, the Reason parameter of the DLLProc procedure contains a value representing the reason that the DLLProc is being called. Table 19.1 lists the possible values of the Reason parameter.
Value | Description |
DLL_PROCESS_DETACH | The DLL is about to unload from memory. |
DLL_THREAD_ATTACH | A process is attaching to the DLL. |
DLL_THREAD_DETACH | A process is detaching from the DLL. |
NOTE: Earlier in this section I said that a DLL will receive a message from Windows when the DLL is initially loaded into memory. It is reasonable to expect that when a DLL loads, the DLLProc is called with a Reason parameter equal to DLL_PROCESS_ATTACH. That doesn't happen, however. Windows defines a DLL_PROCESS_ATTACH message, but Object Pascal does not pass it on to the DLLProc. Instead, Object Pascal calls the DLL's main code block when the DLL_PROCESS_ATTACH message is received. Beca use the main block is called when DLL_PROCESS_ATTACH is received, it is not necessary for Object Pascal to pass that message on to the DLLProc.
The DLL_PROCESS_DETACH message is received only once, just before the DLL unloads from memory. (The DLL_THREAD_ATTACH and DLL_THREAD_DETACH messages might be received many times if a DLL is used by several processes. Processes can include applications, multiple threads in a single application, or other DLLs.) You can use a DLLProc, then, to perform any cleanup code required for the DLL.
Before you use a function or procedure in a DLL, you first have to load the DLL into memory. Loading DLLs at runtime can be accomplished in two ways, as follows:
Both methods have their advantages and disadvantages. I'll explain the differences between static loading and dynamic loading next.
Static loading means that your DLL is automatically loaded when the application that calls the DLL is executed. To use static loading, you declare a function or procedure that resides in the DLL with the external keyword (more on that in the section "Calling Using Static Loading"). The DLL is automatically loaded when the application loads, and you call any functions or procedures exported from the DLL just as you would any other function or procedure. This is by far the easiest way to use code contained in a DLL. The disadvantage to this approach is that if a DLL that the program references is missing, the program will refuse to load.
Dynamic loading means that you specifically load the DLL when needed and unload it when you are done with it. This type of DLL loading has its advantages and disadvantages, too. One advantage is that the DLL is in memory only as long as you need it, so you are making more efficient use of memory. Another advantage is that your application will load more quickly when using dynamic loading because not all the code needed to run the program is loaded when the application initially loads.
The primary disadvantage to using the dynamic loading approach is that it is a bit more work for you. First, you need to load the DLL with the Windows API function LoadLibrary. Then, when you are done with the DLL, you unload it with FreeLibrary. Further (and this is where the work comes in), you need to use the GetProcAddress function to obtain a pointer to the function or procedure you want to call. To say that this approach can be a tad confusing would be an understatement. The following section describes how to call procedures and functions in DLLs using both static and dynamic loading.
The method you use to call a function or procedure in a DLL depends on whether the DLL was statically or dynamically loaded.
Calling functions and procedures from DLLs that are statically loaded is simple. First the calling application must contain a declaration for the function or procedure. After that, you call the function or procedure as you do a regular function or procedure. To import a function or procedure contained in a DLL, use the external modifier in the function or procedure declaration. For example, given the SayHello procedure shown earlier, the declaration in the calling application would look like this:
procedure SayHello(AForm : TForm); external `testdll.dll';
The external keyword tells the compiler that the procedure can be found in a DLL (TESTDLL.DLL in this case). The actual procedure call looks no different than any other:
SayHello(Self);
After you have properly imported the function or procedure, you call it like any other function or procedure. This step presumes, of course, that t he procedure has been exported from the DLL as described earlier.
CAUTION: When you declare functions and procedures contained in a DLL, be sure you get spelling and capitalization right. This is one place in Object Pascal programming where capitalization counts! If you misspell or improperly capitalize a function or procedure name, you will get an exception at runtime and the application will refuse to load.
USING THE external KEYWORDThe external keyword has three flavors. Using external you can import a procedure or function in one of three ways:
- By actual name
- By ordinal value
- By renaming
The first way of importing, by actual name, is the method you have seen used up to this point. You simply declare the name of the function or procedure exactly as it appears in the DLL--for example,
procedure SayHello(AForm : TForm); external `testdll.dll';
The second way of importing, by ordinal value, requires you to specify the ordinal value of the function or procedure as exported from the DLL:
procedure SomeOrdinalProcedure; external `testdll.dll' index 99;
In this case, I am importing the procedure that is exported from the DLL as index 99. I can name the procedure anything I want in the calling application as long as the signature matches that of the procedure in the DLL. I can name the procedure anything I want because the procedure is exported by ordinal value and not by the procedure name.
The third method, by renaming, enables me to import the procedure by it original name, but give the procedure a new name in the calling application. It looks like this:procedure CoolProcedure; external `testdll.dll' name `DoSomethingReallyCool';
Here I am importi ng a procedure called DoSomethingReallyCool and renaming it to CoolProcedure.
Of these three methods, the first, importing by name, is the most commonly used.
The trick in writing and using DLLs, then, is in getting the imports and exports right. Otherwise, there's nothing to it. Unless you need the flexibility that dynamic loading provides, you should almost always opt for static loading.
Calling functions and procedures in DLLs that are dynamically loaded isn't a lot of fun. It requires that you declare a pointer to the function or procedure in the DLL, and pointers to functions can be confusing. To illustrate, let's say you have a procedure in a DLL called SayHello (the SayHello procedure gets a workout in this chapter). It would look like this in the DLL's source code:
procedure SayHello(AForm : TForm); begin MessageBox(AForm.Handle, `Hello From a DLL!', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end;
To call this procedure from your program, you first have to declare a type that describes the procedure:
type TSayHello = procedure(AForm : TForm);
Now that you've done that, you must load the DLL, use GetProcAddress to get a pointer to the procedure, call the procedure, and, finally, unload the DLL. Here's how the whole operation looks:
var DLLInstance : THandle; SayHello : TSayHello; begin { Load the DLL. } DLLInstance := LoadLibrary(`testdll.dll'); { Get the address of the procedure. } @SayHello := GetProcAddress(DLLInstance, `SayHello'); { Call the procedure. } SayHello(Self); { Unload the DLL. } FreeLibrary(DLLInstance); end;
As I said, loading a DLL dynamically is a bit more work. Still, when you need to load a DLL at runtime, that's the way you have to do it. Note that this code is pared down a bit for clarity. You will almost always add some error-checking code to ensure that the DLL loads correctly and that GetProcAddress returns a good address. Here's how the code looks with error checking code in place:
procedure TForm1.DynamicLoadBtnClick(Sender: TObject); type TSayHello = procedure(AForm : TForm); var DLLInstance : THandle; SayHello : TSayHello; begin DLLInstance := LoadLibrary(`testdll.dll'); if DLLInstance = 0 then begin MessageDlg(`Unable to load DLL.', mtError, [mbOK], 0); Exit; end; @SayHello := GetProcAddress(DLLInstance, `SayHello'); if @SayHello <> nil then SayHello(Self) else MessageDlg(`Unable to locate procedure.', mtError, [mbOK], 0); FreeLibrary(DLLInstance); end;
As you can see, you probably won't use dynamic loading of DLLs unless you absolutely have to.
Creating a DLL in Delphi is accomplished through the Object Repository. (The Object Repository is covered on Day 8, "Creating Applications in Delphi."). To create a DLL project, follow these steps:
Were you expecting it to be more complicated than that? Delphi creates the DLL project and displays the Code Editor. The file in the edit window looks like this:
library Project2; { Important note about DLL memory management: ShareMem must be the first unit in your library's USES clause AND your project's (select View-Project Source) USES clause if your DLL exports any procedures or functions that pass strings as parameters or function results. This applies to all strings passed to and from your DLL--even those that are nested in records and classes. ShareMem is the interface unit to the DELPHIMM.DLL shared memory manager, which must be deployed along with your DLL. To avoid using DELPHIMM.DLL, pass string information using PChar or ShortString parameters. } uses SysUtils, Classes; begin end.
Now you can begin adding code to the DLL. Be sure to add any exported procedure or function names to the exports section. Write any standalone functions or procedures that you need in your DLL. When you are finished adding code, you can choose Compile or Build from the Project menu to build the DLL.
COMMENTING THE DLL UNITI want to take a moment to explain the large comment block at the top of a DLL unit. This message is telling you that if your DLL has exported functions and procedures that take a long string type as a parameter or if your exported functions return a long string, you must do the following:
- Put ShareMem at the beginning of the uses list in both the DLL source and the calling application's main unit. Be sure that ShareMem comes before other units in the uses list.
- Ship the Borlndmm.dll file with your DLL. Notice that I said Borlndmm.dll and not Delphimm.dll, as the comments in the DLL code indicate. Borland changed the name of the memory manager DLL but neglected to change the comments generated when you create a new DLL unit. The comments are not completely misleading, however, because both Delphimm.dll and Borlndmm.dll are included with Delphi 4.
To avoid this requirement, just be sure that your DLL functions and procedures don't take any long string parameters and that your DLL functions don't return a long string. Instead of using a long string, you can use a PChar or a short string. For example, rather than use
procedure MyProcedure(var S : string); begin { Procedure code here. } end;
use this:
procedure MyFunction(S : PChar); begin{ Procedure code here. }
end;
This situation is easy enough to work around, so you should never have the need for Borlndmm.dll. You just need to be aware of the restrictions placed on using the long string in DLL functions and procedures. Note that you can use long strings in functions and procedures used within the DLL itself without the need for Borlndmm.dll. This applies only to exported functions and procedures.
NOTE: I always remove the comments about Borlndmm.dll when I create a new DLL. You can certainly leave them in the DLLs source code if you want. After you understand what the comments are saying, you won't need them anymore, so you might as well remove them.
The next three listings contain code that illustrates the concepts discussed thus far. Listing 19.3 contains a DLL that will be called statically from a calling application. Listing 19.4 contains a DLL that is called dynamically from the calling application and implements a DLLProc. Finally, Listing 19.5 contains a program that calls the two DLLs. The program has a form with four buttons that call various procedures in the two DLLs. The book's code (available at http://www.mcp.com/info) contains the sample programs for the projects in these three listings.
library TestDLL; uses SysUtils, Classes, Forms, Windows; procedure SayHello(AForm : TForm); begin MessageBox(AForm.Handle, `Hello From a DLL!', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end; procedure DoSomething; begin MessageBox(0, `This procedure was exported by ordinal.', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end; procedure DoSomethingReallyCool; begin MessageBox(0, `Something really cool.', `DLL Message Box', MB_OK or MB_ICONEXCLAMATION); end; exports SayHello, DoSomething index 99, DoSomethingReallyCool; begin end.
library TestDLL; uses SysUtils, Classes, Forms, Windows; procedure MyDLLProc(Reason: Integer); begin if Reason = DLL_PROCESS_DETACH then { DLL is unloading. Cleanup code here. } Mes sageBox(0, `DLL is unloading!', `DLL Message', MB_OK or MB_ICONEXCLAMATION); end; procedure SayHelloDyn(AForm : TForm); begin MessageBox(AForm.Handle, `Hello From a DLL!' + #13 + `This DLL was loaded dynamically', `DLL Message', MB_OK or MB_ICONEXCLAMATION); end; exports SayHelloDyn; begin DLLProc := @MyDLLProc; end.
unit CallDLLU;
interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) HelloBtn: TButton; OrdBtn: TButton; DynamicLoadBtn: TButton; NamedBtn: TButton; procedure HelloBtnClick(Sender: TObject); procedure OrdBtnClick(Sender: TObject); procedure DynamicLoadBtnClick(Sender: TObject); procedure NamedBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; { A procedure imported by name. } procedure SayHello(AForm : TForm); external `testdll.dll'; { A procedure imported by ordinal. } procedure OrdinalProcedure; external `testdll.dll' index 99; { A procedure imported and renamed. } procedure CoolProcedure; external `testdll.dll' name `DoSomethingReallyCool'; implementation {$R *.DFM} procedure TForm1.HelloBtnClick(Sender: TObject); begin SayHello(Self); end; procedure TForm1.OrdBtnClick(Sender: TObject); begin OrdinalProcedure; end; procedure TForm1.NamedBtnClick(Sender: TObject); begin CoolProcedure; end; procedure TForm1.DynamicLoadBtnClick(Sender: TObject); type TSayHello = procedure(AForm : TForm); var DLLInstance : THandle; SayHello : TSayHello; begin { Load the DLL. } DLLInstance := LoadLibrary(`DynLoad.dll'); { If loading fails then bail out. } if DLLInstance = 0 then begin MessageDlg(`Unable to load DLL.', mtError, [mbOK], 0); Exit; end; { Assign the procedure pointer. } @SayHello := GetProcAddress(DLLInstance, `SayHelloDyn'); { If the procedure was found then call it. } if @SayHello <> n il then SayHello(Self) else MessageDlg(`Unable to locate procedure.', mtError, [mbOK], 0); { Unload the DLL. } FreeLibrary(DLLInstance); end; end.
Your DLLs can contain forms as well as code. There isn't much difference in the way the form is created compared to the way a form in an application is created. First, I'll explain how to write a DLL containing a form. After that, I'll talk about the special case of using an MDI form in a DLL.
Writing a DLL that contains forms is not much more difficult than writing a DLL that contains code only. I believe in learning by example, so in this section, you build a DLL containing a form. Perform these steps:
function ShowForm : Integer; stdcall; var Form : TDLLForm; begin Form := TDLLForm.Create(Application); Result := Form.ShowModal; Form.Free; end;
exports ShowForm;
That's all there is to creating the DLL. Notice that the ShowForm function is declared with the stdcall keyword. This keyword tells the compiler to export the function using the standard call calling convention. Exporting the function as stdcall enables this DLL to be used by development environments other than Delphi.
New Term: Calling conventions specify how the compiler should pass arguments when calling functions and procedures. The five primary calling conventions are stdcall, cdecl, pascal, register, and safecall. See the "Calling Conventions" topic in the Delphi help file for more information on calling conventions.
Notice also that the return value of the DLL's ShowForm function is the value returned from the ShowModal function. This enables you to return some status information to the calling application. Listing 19.6 shows the code for the DLL.
library MyForms; uses SysUtils, Classes, Forms, DLLFormU in `DLLFormU.pas' {DLLForm}; function ShowForm : Integer; stdcall; var Form : TDLLForm; begin Form := TDLLForm.Create(Application); Result := Form.ShowModal; Form.Free; end; exports ShowForm; begin end.
Now the calling application can declare the ShowForm function and call the function. Listing 19.7 shows a Delphi application that calls the MyForms DLL.
unit TestAppU; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; function ShowForm : Integer; stdcall; external `myforms.dll'; implementation {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); begin ShowForm; end; end.
Notice that I used the stdcall keyword again when I declared the function in the calling application. Because the function was exported from the DLL with stdcall, it must also be imported w ith stdcall. You must always import a function or procedure with the same calling convention that you used to export it.
NOTE: If your Delphi DLLs are going to be used only by Delphi applications, you don't need to worry about using stdcall when exporting your functions and procedures. If, however, there is a chance that your DLL will be called from a wide range of applications, you should export your functions and procedures with stdcall.
Having an MDI child form in a DLL is a special case. (MDI forms are discussed on Day 4, "The Delphi IDE Explored.") Let's say you have a Delphi application and the main form is an MDI form. If you try to use an MDI child form contained in a DLL, you will get an exception from VCL that says No MDI forms are currently active. "What? But I have an MDI form in my application!" Not according to VCL, you don't. Here's what happens.
When you attempt to show your MDI child form, VCL checks whether the Application object's MainForm property is valid. If the MainForm property is not valid, an exception is thrown. So what's the problem? The MainForm is valid, right? The problem is that the DLL also contains an Application object, and it is the DLL Application object's MainForm that is checked, not the application's MainForm. Because a DLL doesn't have a main form, this check will always fail.
The fix for this problem is to assign the DLL's Application object to the Application object of the calling application. Naturally, this will work only if the calling application is a VCL application. That's not the whole story, though. Before the DLL unloads, you must also set the DLL's Application object back to its original state. This enables the VCL memory manager to clean up any memory allocated for the DLL. It means that you have to store the DLL's Application object pointer in a global variable in the DLL so that it can be r estored before the DLL unloads.
Let's back up for a moment and review the steps required to show an MDI child form in a DLL:
The first step is easy. Just place the following code near the top of your DLL's source unit:
var
DllApp : TApplication;
Be sure that you place the var keyword below the uses list in the DLL source unit.
Next, create a procedure that will do the TApplication switch and create the child form. The procedure will look like this:
procedure ShowMDIChild(MainApp : TApplication); var Child : TMDIChild; begin if not Assigned(DllApp) then begin DllApp := Application; Application := MainApp; end; Child := TMDIChild.Create(Application.MainForm); Child.Show; end;
Examine this code for a moment. When you call this procedure, you will pass the Application object of the calling application. If the DllApp pointer has not yet been assigned, you assign the DLL's Application object to the temporary pointer. Then you assign the Application object of the calling application to the DLL's Application object. This check ensures that the Application object is set only once. After that, the MDI child form is created, passing the calling application's MainForm as the owner. Finally, the form is shown.
All that remains is to reset the DLL's Application pointer before unloading the DLL. You can use a DLLProc to restore the DLL's application pointer:
procedure MyDLLProc(Reason: Integer); begin if Reason = DLL_PROCESS_DETACH then { DLL is unloadi ng. Restore the Application pointer. } if Assigned(DllApp) then Application := DllApp; end;
Remember, you saved the DLL's Application pointer earlier, so now you just restore it.
As you can see, placing an MDI child form in a DLL requires extra work, but it is certainly possible. The code for the book has an application project called MDIApp and a DLL project called MyForms. These two projects illustrate using an MDI form in a DLL.
Calling a form from a non-VCL application requires a slightly different approach. What you have to do is create a standalone function in the DLL that is called by the calling application. This function can be called from any program provided you declare the function with stdcall. Within the function body, you then create and execute the form. The function looks like this:
function ShowForm : Integer; stdcall;
var
Form : TMyForm; begin Form := TMyForm.Create(Application); Result := Form.ShowModal; Form.Free; end;
Notice that Application is passed for the form's parent. This is the DLL's Application object and serves as the owner of the form. Although I explicitly free the TMyForm object, it isn't strictly necessary because the DLL's Application object will delete the form if I neglect to.
Sometimes it's convenient to have resources contained in a DLL. I've already mentioned internationalization and how a DLL can be used to make your program more easily portable to languages other than the one for which it was designed. Let's say you have an instructions screen for your application and that those instructions are contained in five strings in a DLL. The strings might be named IDS_INSTRUCTION1, IDS_INSTRUCTION2, and so on. You could load and display the strings like this:
LoadString(DllInstance, IDS_INSTRUCTION1, Buff, SizeOf(Buff)); InstructionLabel1.Caption := Buff;
The first parameter of the LoadString function contains the instance handle of the module where the strings can be found. The second parameter contains the ID number of the resource to load. You could create DLLs that contain string resources in several different languages and simply load the appropriate DLL based on a user selection. The code might look like the following:
var DLLName : String; begin case Language of laFrench : DllName := `french.dll'; laGerman : DllName := `german.dll';; laSpanish : DllName := `spanish.dll'; laEnglish : DllName := `english.dll'; end; DllInstance := LoadLibrary(PChar(dllName)); end;
You only have to load the correct DLL; the rest of the code remains the same (assuming the strings have the same identifiers in each of the DLLs, of course). This is just one example of using resources contained in a DLL. You will find many uses for this technique.
You can create a DLL that contains only resources, or you can mix code and resources in the same DLL. Placing resources in a DLL is much the same as adding resources to an application. To create a resource DLL, start a new DLL project and then add a line in the DLL to link the resources:
{$R RESOURC.RES}
That's all there is to creating a resource DLL. I discussed creating resource files on Day 8, so I won't go over that ground again here.
You must have the DLL's instance handle before you can access resources contained in the DLL. If your DLL contains resources only, you will load the DLL dynamically. If your DLL contains resources and code, you might choose to load the DLL statically. If you load the DLL statically, you will still have to call LoadLibrary to get the instance handle:
DllInstance := LoadLibrary(`resource.dll');
Now you can use the instance handle wherever required. The foll owing code loads a bitmap resource contained in a DLL into an Image component:
procedure TMainForm.FormCreate(Sender: TObject); begin DLLInstance := LoadLibrary(`resource.dll'); if DLLInstance <> 0 then begin Image.Picture.Bitmap. LoadFromResourceName(DLLInstance, `ID_BITMAP1'); FreeLibrary(DLLInstance); end else MessageDlg(`Error loading resource DLL.', mtError, [mbOk], 0); end;
Actually, there's not much left to say. To review, though, you know that you can load resource DLLs either statically or dynamically. Either way you have to use LoadLibrary to get the instance handle of the DLL. Don't forget to call FreeLibrary when you are done with the DLL or before your application closes.
NOTE: Using dynamic loading has the added advantage of allowing your application to load faster. In many cases you load the resource DLL only when it's needed and unload it when it's no longer needed. This results in your application using less memory than when the resources are contained in the executable file. The downside to static loading is that your users might see a slight pause when the resource DLL loads. Try to anticipate as much as possible and load the resource DLL when it is least likely to be noticed.
Remember JumpingJack from Day 8? The book's code has a version of JumpingJack that loads the bitmap, sound, and string resources from a DLL. Check out that program for an example of using resources in a DLL.
Using dynamic link libraries isn't as difficult as it might first appear. DLLs are a great way to reuse code. After you create a DLL, you can call it from any application that needs the functionality the code in that DLL provides. Placing VCL forms in a DLL and then calling those forms from a non-Delphi application is a powerful feature. This means that you can create forms that others can call from just about any type of Windows ap plication, whether it is an application written in OWL, MFC, straight C, Visual Basic, and so on. Using resources in DLLs is effective if you have a lot of resources in your application and you want to control when and where those resources are loaded.
The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find the answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."
© Copyright, Macmillan Computer Publishing. All rights reserved.