Have you ever used Adobe photoshop? If you have used it, you will be more familiar with the concept of plug-ins. For laymen, plugins are just code blocks provided to the application from the outside (for example, in a DLL). The difference between a plugin and a normal DLL is that the plugin has the ability to extend the functionality of the parent application. For example, Photoshop itself does not have a large amount of image processing functions. The addition of plug-ins gives it awesome effect such as blur, spot, and all other styles, and none of them have the parent application itself.
This is very good for image processing programs, but why do you need to spend a lot of effort to complete commercial applications that support plugins? Suppose, let's give an example, your application will generate some reports. Your customers will definitely keep asking for updates or adding new reports. You can use an external report generator such as Report Smith, which is a not-so-very-like solution, requires publishing additional files, additional training for users, and so on. You can also use QuickReport, but this will put you in a version control nightmare - if you want to rebuild your application every time you change the font.
However, as long as you make the report into the plugin, you can use it. Need a new report? No problem, just install a DLL and you will see it next time the application starts. Another example is an application that processes data from external devices (such as barcode scanners). To give users more choices, you have to support half a dozen devices. By writing each device interface processing routine as a plug-in, you can achieve maximum scalability without making any changes to the parent application.
getting Started
The most important thing before you start writing code is to figure out what features your application needs to extend. This is because the plugin interacts with the parent application through a specific interface, which will be defined according to your needs. In this article, we will build 3 plugins to showcase several ways the plugin interacts with the parent application.
We will make the plugin into a DLL. Before doing this, though, we have to make a shell to load and test them. Figure 1 shows the test program after the first plug-in is loaded. The first plugin does not accomplish anything big, and in fact, all it does is return a string that describes itself. However, it confirms an important point – it will work properly with or without plug-in applications. If there is no plug-in, it will not appear in the list of installed plug-ins, but the application can still perform functions normally.
The only difference between our plug-in shell and normal applications is the Sharemem unit in the project source file that appears in the uses clause and the code that loads the plug-in file. Any application that passes string parameters between itself and the child DLL? Sharemem unit is required, which is the interface to DelphiMM.dll (Delphi provides this file). To test this shell, you need to copy the DelphiMM.dll file from the Delphi/Bin directory to the path contained in the path environment variable or the directory where the application is located. The file must also be distributed at the same time when the final version is released.
The plug-in is loaded into this test shell through the LoadPlugins process, which is called in the FormCreate event in the main window, see Figure 2. This process uses FindFirst and FindNext functions to find plugin files in the directory where the application is located. After finding a file, use the LoadPlugins process shown in Figure 3 to load it.
{ Find plugin files in the application directory}
PRocedure TfrmMain.LoadPlugins;
var
sr: TSearchRec;
path: string;
Found: Integer;
Begin
path := ExtractFilePath(application.Exename);
try
Found := FindFirst(path + cPLUGIN_MASK, 0, sr);
while Found = 0 do begin
LoadPlugin(sr);
Found := FindNext(sr);
end;
Finally
FindClose(sr);
end;
end;
{ Load the specified plugin DLL. }
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
Begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle $#@60;$#@62; 0 then
Begin
DescribeProc := GetProcAddress(LibHandle, cPLUGIN_DESCRIBE);
If Assigned(DescribeProc) then
DescriptionProc(Description);
memPlugins.Lines.Add(Description);
end
else
Begin
MessageDlg('File "' + sr.Name +'" is not a valid plug-in.',
mtInformation, [mbOK], 0);
end;
end
else
MessageDlg('An error occurred loading the plug-in "' +
sr.Name + '".', mtError, [mbOK], 0);
end;
The LoadPlugin method demonstrates the core of the plug-in mechanism. First, the plugin is written as a DLL. Secondly, it is loaded dynamically through the LoadLibrary API. Once the DLL is loaded, we need a way to access the procedures and functions it contains. API calls GetProcAddress provide this mechanism, which returns a pointer to the required routine. In our simple demonstration, the plugin only contains a procedure called DescribePlugin, specified by the constant cPLUGIN_DESCRIBE (the case of the procedure name is very important, and the name passed to GetProcAddress must be exactly the same as the routine name included in the DLL). If no requested routine is found in the DLL, GetProcAddree will return nil, so you will agree to use the Assigned function to determine the return value.
In order to store pointers to a function in an easy-to-use way, it is necessary to create a specific type for the variables used. Note that the return value of GetProcAddress is stored in a variable, DescribeProc, belongs to the TpluginDescribe type. Here is its statement:
type
TPluginDescribe = procedure(var Desc: string); stdcall;
Since the procedure exists inside the DLL, it compiles all export routines through standard call conversions, so the stdcall indicator is required. This process uses a var parameter, which contains the plugin description when the process returns.
To call the process you just obtained, just use the variable that saves the address as the process name, followed by any parameters. In our case, the statement:
DescriptionProc(Description)
The description process obtained in the plug-in will be called and the Description variable is populated with a string describing the plug-in's functionality.
Construction plug-in
We have created the parent application, and it's time to create the plugin we want to load. The plugin file is a standard Delphi DLL, so we create a new DLL project from the Delphi IDE and save it. Since the exported plug-in function will use string parameters, the Sharemen unit should be placed first in the use clause of the project. Figure 4 lists the project source file of our simple plug-in.
uses
Sharemem, SysUtils, Classes,
main in 'main.pas';
{$E plg.}
eXPorts
DescribePlugin;
Begin
end.
Although the plugin is a DLL file, there is no need to give it a .DLL extension. In fact, one reason is enough to give us a reason to change the extension: when the parent application looks for the file to be loaded, the new extension can serve as a specific file mask. By using another extension (our example uses *.plg), you can be somewhat confident that the application will only load the corresponding files. Compile indicator $X can achieve this change, or you can set the extension through the Application page of the Project Options dialog box.
The code for the first example plugin is very simple. Figure 5 shows the code contained in a new unit. Note that the DescribePlugin prototype is consistent with the TpluginDescribe type in the shell application, and uses the additional export reserved word to specify that the process will be exported. The exported process name will also appear in the exports section of the main project source code (listed in Figure 4).
unit main;
interface
procedure DescribePlugin(var Desc: string);
export; stdcall;
Implementation
procedure DescribePlugin(var Desc: string);
Begin
Desc := 'Test plugin v1.00';
end;
end.
Before testing this plugin, copy it to the path of the main application. The easiest way is to create a plug-in in the subdirectory of the home directory, and then set the output path to the main path (the Directories/Conditionals of the Project Options dialog box can also be used for this setting).
debug
Now let me introduce a better feature in Delphi 3: the ability to debug DLLs from the IDE. In a DLL project, you can specify a program as the host application through the Run Paramates dialog box. This is the path to the application that will call the DLL (in our example, the test shell just created). Then you can set breakpoints in your DLL code and press F9 to run it - just like you would do in a normal application. Delphi will run the specified host program and, by compiling the DLL with debug information, direct you to the breakpoint in the DLL code.