bclogo_ws.gif (5716 bytes)Object Packages Enable Highly Modular Applications

by James Heyworth

Paper presented to the Inprise Asia-Pacific Conference, October 13 1998

 

 

 

Jamesh4.gif (25086 bytes)

Introduction

Object packages in Delphi and C++ Builder are the cornerstone of modularity. Every developer uses VCL packages in the IDE to dynamically load Borland and third party libraries of VCL components. Packages are less frequently used by application developers who may prefer the size and discrete containment of a single executable. For some applications, packages are highly desirable. Packages can be used to implement the same dynamic addin capability seen in Delphi and C++ Builder.

Using addin packages we can build an extensible application framework and then rapidly deploy incremental improvements in software function.

This paper presents a framework for building addin packages and a prototype of a visual interface for modular business applications.

We will cover the background to using, optimising and dynamically loading packages; and review several examples of addin package construction.

We will see how VCL form inheritance can be used to allow addin modules to supply menus, toolbars and forms to the container application. The end result is faster construction of a visually appealing user interface, coupled with flexible addin functionality.

 

When to Use Packages?

Packages are most useful in the following scenarios:

If you're looking to build small standalone programs that run anywhere off a floppy disk then you’re better off not using packages. When compiling a standalone executable, the compiler optimizer will remove redundant units and functions not actually referenced in code. A package on the other hand is a complete compilation of all contained units and functions, with the addition of an interface. So a small application using runtime packages might end up shipping a lot of redundant code. With large applications the relative size and percentage redundant code often becomes insignificant, particularly when other benefits are taken into account.

 

Requirements for Addin Packages

Addins offer particular opportunities to develop and deploy applications:

The application requirements for an addin interface can vary widely depending on the application, for example:

There are four key requirements for an addin environment:

The standard VCL provides functions to load and unload packages. The other requirements tend to be application specific, depending on the purpose of the addin packages.

In the context of a large business application you also need a stable core of underlying components and application services that can be shared by the application and modules.

To allow independant, modular development the interface layer can be separated from the main application. A typical package framework might look like this:

Figure 1. Software layers

To allow addins to be removed, the main application must operate without any addins present. The application typically may provide a number of interfaces that may or may not be supported or used by a particular addin package.

 

What's in a Package?

Runtime packages are DLLs that implement additional Delphi VCL interfaces. They export package information, units, classes, properties, methods, types, variables and constants in a standard interface format. The compiler also generates additional designtime interface information in Delphi DCP or C++ Builder BPI files. Figure 2 below illustrates part of a package interface, which you can view using the TDump utility.

Figure 2. Contents of a VCL package interface.

 

Optimising Packages

When you start using runtime packages, the standard approach is to use the VCL packages and third party component libraries in the Delphi development environment.

In some cases you can reduce installed code size and make applications load faster by repackaging units into your own custom runtime package. To do this, create a new package, adding all of the base units your code uses. The compiler will give warnings for any additional units implicitly added to the package. Make sure you give the package a unique name, that doesn't conflict with Delphi or other known package names. A repackaged library can only be used for runtime distribution, since the VCL will not load packages containing duplicate unit names.

To build the executable, remove the standard packages from the runtime package list in the project options and replace them with your custom runtime package.

The custom package in Figure 2 illustrates custom unit packaging for a very small "hello world" application. In this case the final package is about half the size of the Delphi VCL40.DPL, saving nearly 1MB in file size. This example is artificial, but illustrates the point that VCL packages built for the Delphi IDE are not necessarily the optimal ones for your applications. However if your are in doubt about this or intend to ship future applications that use the same VCL component libraries, you may be best off using the standard VCL packages.

See Sample1 in the accompanying source code.

Figure 3. A custom runtime package.

 

VCL Functions for Dynamically Loading Packages

When you compile an application using runtime packages, the compiler generates static imports of functions in the package interfaces. Your application will automatically load and initialize the package, failing when the package is not found. For addins we want an application to function with the package and then load or unload the package under program control.

Runtime package functions are located in the SysUtils unit of the VCL:

 

{ LoadPackage loads a given package DLL, checks for duplicate units and

calls the initialization blocks of all the contained units }

function LoadPackage(const Name: string): HMODULE;

{ UnloadPackage does the opposite of LoadPackage by calling the finalization

blocks of all contained units, then unloading the package DLL }

procedure UnloadPackage(Module: HMODULE);

 

Information functions, GetPackageDescription and GetPackageInfo may also be useful.

 

VCL Class Registration Functions

Class registration allows you to convert a string containing a class name into a class reference and then create instances of the class. This technique is a useful abstraction, allowing classes to be defined in one package and referenced in another separate package, without needing to resolve the reference at compile time.

{ Class registration routines }

procedure RegisterClass(AClass: TPersistentClass);

procedure RegisterClasses(AClasses: array of TPersistentClass);

procedure RegisterClassAlias(AClass: TPersistentClass; const Alias: string);

procedure UnRegisterClass(AClass: TPersistentClass);

procedure UnRegisterClasses(AClasses: array of TPersistentClass);

procedure UnRegisterModuleClasses(Module: HMODULE);

function FindClass(const ClassName: string): TPersistentClass;

function GetClass(const AClassName: string): TPersistentClass;

When unloading a package, classes can be unregistered explicitly in a finalization section, or implicitly using the UnRegisterModuleClasses function.

Note: All of the above are also available through the C++ Builder VCL. Check SysUtils.hpp and Classes.hpp for details.

 

Putting it all Together

To use packages dynamically, all we absolutely have to do is call the LoadPackage and UnloadPackage functions. However if the loaded package registers classes and creates objects we will have a little more work to do when unloading.

When LoadPackage executes, any code in Delphi unit initialization sections is also executed. This is often a handy place to register form classes.

 

Figure 4. Steps to dynamically load/unload a package.

 

For C++ Builder, two preprocessor pragmas can be used to emulate Delphi's initialization and finalization sections. To use these, the function names must be unique in each unit. Here is an example:

void initUnit2(void)

{

RegisterClass(__classid(TfrmCustMaint));

}

void finalUnit2(void)

{

UnRegisterClass(__classid(TfrmCustMaint));

}

#pragma startup initUnit2

#pragma exit finalUnit2

 

When unloading a package you need to manage your own garbage collection - freeing all objects (class instances) created from the package. To do this, you will need to ensure that all objects have an owner or are otherwise identifiable, and are freed during finalization.

 

The following function can be used to clean up Forms owned by the Application:

procedure DestroyFormInstances(ModuleInstance: HMODULE);

var

  i: integer;

begin

  with Application do

    for i := ComponentCount - 1 downto 0 do

      if HMODULE(FindClassHInstance(Components[i].ClassType)) = FModuleInstance then

        Components[i].Free;

end;

The underlying function used here is declared in the System unit:

function FindClassHInstance(ClassType: TClass): Longint;

 

A Simple Package Example

See Demo1 in the accompanying source code. Thanks to the Delphi developers at Inprise for supplying the basis for this example, available on the Inprise web site.

The techniques described above are illustrated in this example. A single package is loaded and form instances are created from the package by reference to a known class name. Before the package unloads, form instances are destroyed and classes are unregistered.

While this illustrates the steps to dynamically load and unload packages, there are several more requirements for a scaleable framework.

 

Prototyping a GUI Framework

We'll look at a prototype for a GUI business application - an application with many forms, using RAD style data bound components. The MDI framework provides a convenient container for the GUI. For a first cut we can add a simple menu to the main form of the application. Each addin can add menu items to the main form menu, using a structured interface. The interface module provides standard functions; a form to load and unload packages; and methods to read/write the Windows Registry.

Addin Interfaces are negotiated through a package containing an interface unit and a form for adding and removing packages at runtime.

The AddinInterface unit contains the following items:

See Demo2 in the accompanying source code for full details of these interfaces.

Details of Addin packages are recorded in the Windows Registry and maintained using a simple form. The form uses functions in the AddinInterface unit to retrieve and update loaded packages.

Figure 5. Storing addin package information in the Registry.

 

Figure 6. Loading and unloading packages at runtime.

 

In the Demo2 prototype, an addin package registers itself with the AddinInterface unit when it is initialized. It creates a new TAddinModuleInterface instance through the interface and then adds string information about menu items and the forms they create. The AddinInterface unit merges this information into the main menu of the application, creating a TAddinFormInterface instance for each menu item. At runtime the TAddinFormInterface class executes the form used by the menu item.

var

  ThisInterface: TAddinModuleInterface;

initialization

  ThisInterface := InitAddinModuleInterface(HInstance);

  with ThisInterface do

  begin

    Caption := 'Module &1';

    AddMaintenanceForm('&Customers', 'TfrmCustMaint');

    AddMaintenanceForm('&Employees', 'TfrmEmpMaint');

    AddEnquiryForm('&Sales Orders', 'TfrmOrderEnquiry');

    AddReportForm('&Phone List', 'TfrmCustomerPhoneList');

    MergeUserInterface;

  end;

While this achieves our objective, the process of initializing addins and itemising their user interface could be simplified, made more visually appealing, and more flexible.

 

A Problem With Used Packages and Garbage Collection

The Demo2 application introduces a bug which highlights issues associated with package dependencies. A general protection fault (GPF) occurs after previewing a report in the Addin1 package, and then unloading that package. Debugging identified that this package uses another component library for Quick Reports. A report preview form owned by the Application is not freed when the Addin1 package unloads.

There are several possible workarounds to this problem:

Visual Design of Addins With a More Appealing Interface

See Demo3 in the accompanying source code.

We can use Delphi and C++ Builder's visual form inheritance to create a more versatile and attractive interface. We will replace the interface construction parts of the previous example with a base form class that contains an interface design. Elements in the base form class include:

You could also add other styles of user interface, customised to your application.

In each Addin package we create a descendant of this class, adding interface elements and the code to implement menu actions. Doing it visually allows the addin to be constructed much more like a standalone application.

To merge the Addin descendant interface,we create a class type reference to the ancestor interface class, then pass the descendant class to the interface package. Initialization of the Addin package is simplified to just a single line of code:

initialization

InitAddinModuleInterface(HInstance, 'Module &1', TfrmAddinInterface1);

To merge the addin interface with the application we use some special code in the interface package. First we create an invisible instance of the addin interface form. Components are then moved from the interface form and placed on the application main form, by moving menu items and changing the Parent of visual toolbar controls.

The VCL supports simple merging of menus in an MDI framework using a property setting, however the following code is more flexible and allows us to leave out unused items in the descendant menu.

for i := MainMenu1.Items.Count - 1 downto 0 do

begin

  ThisItem := MainMenu1.Items[i];

  with ThisItem do

  if (Action <> nil) or Assigned(OnClick) or (Count > 0) then

  begin

    if (ThisItem.Parent <> nil) then

      ThisItem.Parent.Remove(ThisItem);

    ModuleMenuItem.Insert(0, ThisItem);

  end;

end;

Moving the toolbar is fairly straightforward:

with Toolbar1 do

begin

  Align := alBottom;

  Parent := Application.MainForm;

  Align := alTop;

end;

Note that these interface components are still owned by the invisible addin form. The addin package remains completely responsible for the execution of its actions or menu items, giving us much greater modularity and simplifying the interface to a visual construction process.

The resulting runtime application is much more visually attractive:

Image7.gif (22326 bytes)

Figure 7. The visual addin interface at runtime.

 

Developing New Addin Packages

In this framework, new addin packages can be developed relatively independantly. However to create descendent classes of the interface form, developers need access to the interface unit.

The interface can be added to the repository for relatively easy use. Another approach might use a form expert to generate the descendant interface form class. (this is work in progress).

See Demo4 for an example addin package.

 

Opportunities for Further Enhancement

Reviewing this design prototype, I would make a couple of observations:

 

References

Inprise Corporation 1995 - 1998, VCL source code for Delphi 4 and C++ Builder 3. These units in particular: System, SysInit, SysUtils, Classes.

Inprise Corporation 1997, Dynamic Package Loading, web site sample at http://www.inprise.com/devsupport/delphi/downloads/index.html

This was used to develop the Demo1 sample application.

 

Downloads

Paper and Compiled Demo Programs (1.2MB)

Paper and Source Code Only (600kB)