Home Articles Books Downloads FAQs Tips

Advanced Package Creation: Solving the problem of unwanted component exports

This article addresses a number of definciies(spell check) in the way BCB builds component packages. Specifically, this article focuses on the problem of components being needlessly exported from projects that use them. The unwanted exports cause two main problems. First, if you are building a DLL that uses third party C++ or pascal components, the unwanted exports can interfere with client EXE's that use the same components (an example of this scenario appears later in the article). Secondly, the unwanted exports may cause tremendous bloating of projects that use them.

Although this article deals mostly with package creation, it also discusses how to fix a third party component library so that its components won't be needlessly exported.

Tip Note:

This article is intended for power BCB users. Prior to the publishing of this article, I am not aware of anyone who has successfully solved the problem of gratuitous component exports in BCB. Until now, the consensus among the BCB community has been that this problem is not solvable, and that you just had to live with the unwanted component exports and the bloating that results from them.

Although I have found a solution to this problem, it is not a simple one. The solution involves minor changes to the VCL header files, Using makefiles to build your own components, and using a utility program to modify component libraries from third party vendors. If any of these ideas make you queasy (they probably should), and if unwanted component exports have not be a problem for you, then I suggest that you not bother reading this article.



Summary: The problem with packages in C++Builder

The sample components package in BCB5 and BCB6 contains a handful C++ components. The sample components are built from an IDE package project. Because of this, the sample components suffer from the same problems as user packages.

To see the problems with BCB packages, simply drop a sample component, such as TPie, on a newly created application. Edit the project options and turn off runtime packages. Next, save and compile the project. Open a console window and run TDUMP to view the list of symbols exported from the executable. The -ee switch tells TDUMP to list only the exported symbols. A sample of the output from TDUMP is shown below:

$ tdump -ee project1.exe
Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation
                   Display of File PROJECT1.EXE

EXPORT ord:0026='__tpdsc__ TCCalendar'
EXPORT ord:0099='__tpdsc__ TPerformanceGraph'
EXPORT ord:0048='__tpdsc__ Cdiroutl::TCDirectoryOutline'
EXPORT ord:0115='__tpdsc__ TPie'
EXPORT ord:0116='__tpdsc__ TAngles'
EXPORT ord:0069='__tpdsc__ TCGauge'
EXPORT ord:0029='__linkproc__ Ccalendr::Finalize'
EXPORT ord:0028='__linkproc__ Ccalendr::Initialize'
EXPORT ord:0050='__linkproc__ Cdiroutl::Finalize'
EXPORT ord:0049='__linkproc__ Cdiroutl::Initialize'
EXPORT ord:0071='__linkproc__ Cgauges::Finalize'
EXPORT ord:0070='__linkproc__ Cgauges::Initialize'
EXPORT ord:0101='__linkproc__ Perfgrap::Finalize'
EXPORT ord:0100='__linkproc__ Perfgrap::Initialize'
EXPORT ord:0118='__linkproc__ Pies::Finalize'
EXPORT ord:0117='__linkproc__ Pies::Initialize'
EXPORT ord:0003='__linkproc__ Unit1::Finalize'
EXPORT ord:0002='__linkproc__ Unit1::Initialize'
...
EXPORT ord:0031='__fastcall Cdiroutl::TCDirectoryOutline::AssignCaseProc()'
EXPORT ord:0032='__fastcall Cdiroutl::TCDirectoryOutline::BuildOneLevel(long)'
EXPORT ord:0034='__fastcall Cdiroutl::TCDirectoryOutline::BuildSubTree(long)'
EXPORT ord:0033='__fastcall Cdiroutl::TCDirectoryOutline::BuildTree()'
...
EXPORT ord:0123='TCGauge::'
EXPORT ord:0051='__fastcall TCGauge::TCGauge(Classes::TComponent *)'
EXPORT ord:0068='__fastcall TCGauge::AddProgress(long)'
EXPORT ord:0052='__fastcall TCGauge::GetPercentDone()'
EXPORT ord:0053='__fastcall TCGauge::Paint()'
...
EXPORT ord:0094='__fastcall TPerformanceGraph::FirstY()'
EXPORT ord:0073='__fastcall TPerformanceGraph::GetBandCount()'
EXPORT ord:0075='__fastcall TPerformanceGraph::Initialize(long)'
...
EXPORT ord:0125='TPie::'
EXPORT ord:0108='__fastcall TPie::TPie(Classes::TComponent *)'
EXPORT ord:0109='__fastcall TPie::~TPie()'
EXPORT ord:0114='__fastcall TPie::Paint()'
EXPORT ord:0113='__fastcall TPie::SetAngles(TAngles *)'
EXPORT ord:0111='__fastcall TPie::SetBrush(Graphics::TBrush *)'
EXPORT ord:0112='__fastcall TPie::SetPen(Graphics::TPen *)'
EXPORT ord:0110='__fastcall TPie::StyleChanged(System::TObject *)'
EXPORT ord:0127='_Form1'
EXPORT ord:0001='__GetExceptDLLinfo'
EXPORT ord:0119='___CPPdebugHook'
I have created two packages to illustrate the problem with packages in C++Builder. The first package is called test_rt.bpk, and it is a runtime only package. The second package is a design time only packaged called test_dt.bpk. The design time package contains code that registers the components in the runtime package. The is the package configuration that Borland advocates. See faq94.htm for more information on how to create runtime and design time packages.

The runtime package contains three components. Two of the components, TComponent1 and TComponent2, are C++ components. The third component, TComponent3, is a pascal component. I have included a pascal component because although BCB treats pascal components differently, they still suffer from the unwanted export problem.

The source for TComponent1 and TComponent3 is shown below. I have omitted the second C++ component, TComponent2, to save space. The source for TComponent2 is nearly identical to that of TComponent1, with the exception that Foo1 and Bar1 have been renamed to Foo2 and Bar2 respectively. All of the source code, including the source for TComponent2 can be dowloaded from the links at the bottom of the article.

//-------------------------------------------------------------------
// Listing 1: source code for TComponent1
// component1.h
#ifndef Component1H
#define Component1H

#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>

class PACKAGE TComponent1 : public TComponent
{
private:
protected:
public:
    __fastcall TComponent1(TComponent* Owner);

    void __fastcall Foo1();
    void __fastcall Bar1();

__published:
};

#endif
//-------------------------------------------------------------------

//-------------------------------------------------------------------
// component1.cpp
#include <vcl.h>
#pragma hdrstop

#include "Component1.h"

#pragma package(smart_init)

//-------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not
// have any pure virtual functions.
//

static inline void ValidCtrCheck(TComponent1 *)
{
    new TComponent1(NULL);
}

__fastcall TComponent1::TComponent1(TComponent* Owner)
    : TComponent(Owner)
{
}

void __fastcall TComponent1::Foo1()
{
}

void __fastcall TComponent1::Bar1()
{
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
// Listing 2: source code for TComponent3, a pascal component
unit component3;

interface

uses
  Windows, Messages, SysUtils, Classes;

type
  TComponent3 = class(TComponent)
  public
    procedure Foo3;
    procedure Bar3;
  end;

implementation

procedure TComponent3.Foo3;
begin
end;

procedure TComponent3.Bar3;
begin
end;

end.

Note that the component source does not contain code for registering the component. This is because the component source resides in a runtime only package. The code for registering the components belongs in the design time package test_dt.bpk.

  1. Create a DLL with MSVC that flattens the C++ classes into plain C functions. The plain C functions will be imported from BCB.
  2. Create a COM object with MSVC that wraps the C++ classes via containment. BCB will be a COM client of the VC++ COM object.
  3. Wrap the C++ classes using an abstract class with nothing but virtual functions in it. This is essentially COM without the ugly parts.

Each technique is described in more detail below. In each example, we will assume that the MSVC DLL exports a class that looks like this:

class CFoo
{
public:
    CFoo(int x);
    ~CFoo();

    int DoSomething(int y);
};

Technique 1: Flattening the C++ class into a C library

From the previous article on VC++ DLLs, we know that it is possible for a Borland project to call plain C functions that are exported from an MSVC DLL. Using this information, we can create a DLL project in MSVC that exports plain C functions for use in BCB. This MSVC wrapper DLL will be a client of the C++ DLL. The wrapper DLL will export plain C functions for creating CFoo objects, for calling CFoo member functions, and for deleting the CFoo object.

The CFoo class contains three functions that we care about: the constructor, the destructor, and the all important DoSomething function. We need to flatten each of these into an equivalent C function.

// original class
class CFoo
{
public:
    CFoo(int x);
    ~CFoo();

    int DoSomething(int y);
};

// flattened C code
void* __stdcall new_CFoo(int x)
{
    return new CFoo(x);
}

int __stdcall CFoo_DoSomething(void* handle, int y)
{
    CFoo *foo = reinterpret_cast<CFoo *>(handle);
    return foo->DoSomething(y);
}

void __stdcall delete_CFoo(void *handle)
{
    CFoo *foo = reinterpret_cast<CFoo *>(handle);
    delete foo;
}

There are lots of important things to notice here. First, note that each C++ member function has been mapped to a plain C function. Second, observe that we explicitly use the __stdcall calling convention for the C functions. From the previous DLL article, we know that simply calling plain C functions in an MSVC DLL can be a real chore. If we put our past sufferings to use, we can make this endeavor a little bit easier. The easiest way to for Borland to call a Microsoft DLL is if that DLL exports plain, undecorated, C functions that use the __stdcall calling convention. Borland and Microsoft don't agree on __cdecl functions. Normally, they don't agree on __stdcall functions either because MSVC decorates __stdcall functions, but we can suppress that behavior by adding a DEF file to the MSVC project. See the examples from the download section for an example of the DEF file.

Another thing to note about the code is that the new_CFoo function returns a pointer to the CFoo object. The BCB caller must store this pointer in some location. This may seem contradictory to the gist of this article. After all, I thought that BCB couldn't use the C++ objects from an MSVC DLL? If that's true, then why are we returning a pointer to a CFoo object?

The answer is that BCB cannot call the member functions of an class exported by an MSVC DLL. However, that doesn't mean that it can store the address of such an object. That's what new_CFoo returns, a pointer to a CFoo object. The BCB client can store that pointer, but there isn't much that it can do with it. It can't dereference it (and should not attempt to do so). To make this concept easier to understand, new_CFoo returns a void pointer (it can't really return anything else anyway). On the BCB side, there is not much you can safely do with this void pointer, other than storing it and passing it back into the DLL.

Ok, two more quick notes before we move on. First, notice that CFoo_DoSomething takes a void pointer argument as its first parameter. This void pointer is the same void pointer that is returned by new_CFoo. The void pointer is cast back into a CFoo object using reinterpret_cast (you know you're dealing ugly code when you see a reinterpret_cast). The DoSomething member function is called after the cast. Lastly, note that the void pointer is also an argument to the delete_CFoo function. It is crucial that the wrapper DLL delete the object. You should not call delete on the void pointer from BCB. That will certainly not do what you want it to.

The listing below shows a DLL header file for the C functions. This header file can be shared between MSVC and BCB.

// DLL header file
#ifndef DLL_H
#define DLL_H

#ifdef BUILD_DLL
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#else

#ifdef __cplusplus
extern "C" {
#endif

DLLAPI void* __stdcall new_CFoo(int x);
DLLAPI int   __stdcall CFoo_DoSomething(void* handle, int y);
DLLAPI void  __stdcall delete_CFoo(void *handle);

#ifdef __cplusplus
}
#endif

#endif

This is a typical DLL header file. One interesting thing to notice is that you don't see the CFoo class anywhere in the header file. The header file contaisn only plain C functions that will wrap CFoo.

The next listing shows how to call the DLL from BCB

#include "dll.h"

void bar()
{
    int x = 10;
    int y = 20;
    int z;

    void * foo = new_CFoo(x);
    z = CFoo_DoSomething(foo, y);
    delete_CFoo(foo);
}

That's it. Pretty it isn't, but it does work. In fact, despite the grotesqueness of this technique, it is amazingly handy in other situations that don't even involve DLLs. For instance, Delphi programmers employ this same technique because Delphi cannot call C++ member funtions. Delphi programmers must flatten the C++ class into C code, and then link with the C OBJ files. The open source SWIG (swig.org) tool is designed to generate wrapper functions like this, which allows you to call C++ objects from scripting languages such as Python.

Technique 2: Create a COM wrapper

Unfortunately, I don't have an example for this technique yet (hey, I said the article wasn't ready for prime time). But the idea works like this. You create a COM object in MSVC. There is probably a wizard that you can run. Create an inprocess server (ie a DLL, not an EXE). Also, make sure you create a COM object, and not an automation object. Automation just makes everything more difficult. Unless you also need to use the C++ class from VB or an ASP page, use plain COM and not automation.

Inside the COM project, create a new COM object. MSVC will probably want you to create an COM interface. Since we are wrapping a class called CFoo, a good interface name would be IFoo. MSVC will also want you to name the implementation class for the COM obect. CFooImpl is a good candidate.

The COM object should wrap the C++ DLL class using aggregation. In other words, the COM object contains a CFoo member variable. Don't try to inherit your COM class from CFoo. For each member function of the C++ DLL class (CFoo), create a similar function in your COM object. If possible, use the same name, pass the same arguments, and return the same type of value. You will need to tweak some things. For example, strings are usually passed as BSTR's in COM. Also, return values are typically passed as out parameters, because the COM method should return an error code. When you are done, each member function of the C++ class should have a corresponding function in the COM wrapper.

After you build the COM wrapper, register it with regsrv32.exe. Once you do that, you should be able to instantiate the COM object and call its wrapper member functions from your BCB code.

Once again, I apologize for not having a working demo of this technique ready to go.

Technique 3: Use an abstract base class with virtual functions (pseudo-COM)

Technique 3 is a pseudo-COM approach. COM is a binary object specification. A COM object can be called from both BCB and MSVC, regardless of the compiler that the COM object was compiled with. So how does this binary magic work? The answer is the foundation for this technique.

COM function calls are dispatched via a function lookup table. Miraculously, this function lookup table works exactly the same way that virtual function tables work in C++. In fact, they are one in the same. COM is a glorified form of virtual functions and vtables.

COM works because BCB and MSVC employ exactly the same virtual dispatching system. COM relies on the fact that most Win32 C++ compilers all generate and use vtables the same way. Because the two compilers use the same virtual dispatching system, we can create a wrapper class with virtual functions in MSVC that can be called from BCB. This is exactly what COM does.

Here is the DLL header file for the pseudo-COM wrapper class. It consists of an abstract base class, IFoo, that serves as the pseudo-COM interface. It also consists of two C functions for creating and deleting IFoo objects. This header file is shared between MSVC and BCB.

#ifndef DLL_H
#define DLL_H

#ifdef BUILD_DLL
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

// psuedo COM interface
class IFoo
{
public:
    virtual int __stdcall DoSomething(int x) = 0;
    virtual __stdcall ~IFoo() = 0;
};

#ifdef __cplusplus
extern "C" {
#endif

DLLAPI IFoo*  __stdcall new_IFoo(int x);
DLLAPI void   __stdcall delete_IFoo(IFoo *f);

#ifdef __cplusplus
}
#endif

#endif

Notice that the two C functions resemble the functions from Technique 1, except that now they work with IFoo pointers instead of unsafe void pointers. This technique provides a little more type safety than the first.

Here is the source code for the MSVC wrapper. It contains a class call CFooImpl that inherits from IFoo. CFooImpl is an implementation of the IFoo interface.

#define BUILD_DLL

#include "dll.h"

IFoo::~IFoo()
{
	// must implement base class destructor
	// even if its abstract
}

// Note: we declare the class here because no one outside needs to be concerned
//       with it.
class CFooImpl : public IFoo
{
private:
    CFoo  m_Foo; // the real C++ class from the existing MSVC C++ DLL
public:
    CFooImpl(int x);
    virtual ~CFooImpl();
    virtual int __stdcall DoSomething(int x);
};

CFooImpl::CFooImpl(int x)
    : m_Foo(x)
{
}

int __stdcall CFooImpl::DoSomething(int x)
{
    return m_Foo.DoSomething(x);
}

CFooImpl::~CFooImpl()
{
}

IFoo * __stdcall new_IFoo(int x)
{
    return new CFooImpl(x);
}

void __stdcall delete_IFoo(IFoo *f)
{
    delete f;
}

There is lots of good stuff going on here. First of all, notice that now we have a class in the header file being shared between BCB and MSVC. That seems like it ought to be a good thing. More important than that, notice that the BCB project will only interact with the IFoo class. The actual implementation of IFoo is provided by a derived class call CFooImpl, which is internal to the MSVC wrapper project.

The BCB client code will work with IFoo objects polymorphically. To obtain a wrapper instance, the BCB code will call the new_IFoo function. new_IFoo works like a factory function, serving up new IFoo instances. new_Foo returns a pointer to an IFoo instance. However, that pointer is polymorphic. The static type of the pointer is IFoo, but its actual dynamic type will be a pointer to a CFooImpl (a fact that is unbeknownst to the BCB code).

Here is the code for the BCB client.

#include "dll.h"

void bar()
{
    int x = 10;
    int y = 20;
    int z;


    IFoo *foo = new_IFoo(x);
    z = foo->DoSomething(y);
    delete_IFoo(foo);
}

Now some parting comments on technique 3. First, it is crucial that you delete the IFoo pointer from the MSVC DLL. This is done by passing the IFoo pointer to the delete_IFoo function. Don't attempt to delete the object from BCB.

void bar()
{
    IFoo *foo = new_IFoo(x);
    delete foo;               // BOOM!!!
}

This code will surely die an agonizing death. The problem is that IFoo was created from within the new_IFoo function in the MSVC wrapper DLL. As such, the memory for the IFoo object is allocated by the MSVC memory manager. When you delete an object, it is important that you delete it with the same memory manager that was used to create it. If you call delete on the pointer from the BCB side, then you will be deleting it with Borland memory manager. Now, I could be wrong, but I would bet my house and a reproductive organ or two that the Microsoft memory manager and the Borland memory manager don't conspire to work together. When you delete the pointer with the Borland memory manager, it is doubtful that it will attempt to contact the Microsoft memory manager to let it know that it should free some memory.

Another item of note is that the BCB code works entirely in terms of the IFoo abstract interface. You don't see any occurrence of the class CFooImpl on the BCB side. CFooImpl is internal to the MSVC wrapper project. When you call DoSomething from the BCB side, the call is dispatched to CFooImpl via the virtual table.

If you are having troubling understanding this concept, don't worry. I am probably not describing it very well. To help understand what is going on, you might want to step through the code on the BCB side using the CPU viewer. This will allow you to step through each assembly instruction and see how the vtable lookup works.

Tip Note:

If you employ this pseudo-com technique, make sure that you do not attempt to overload virtual functions. In other words, don't create an interface that looks like this:

class IFoo
{
public:
    virtual int __stdcall DoSomething(int x) = 0;
    virtual int __stdcall DoSomething(float x) = 0;
    virtual int __stdcall DoSomething(const char *x) = 0;
};
The reason you don't want to overload the virtual interface functions is that MSVC and BCB may not (and probably won't) order the functions the same way in the vtable. When I tested overloading, calling DoSomething(int) on the BCB side seemed to get dispacted to DoSoemthing(float) on the MSVC side. Borland and Microsoft appear to agree on vtable formats as long as you don't overload. This may explain why you don't seem COM objects with overloaded functions.

If you need to wrap a C++ class with overloaded functions, then you should create a distinct function name for each one.

class IFoo
{
public:
    virtual int __stdcall DoSomething_int  (int x) = 0;
    virtual int __stdcall DoSomething_float(float x) = 0;
    virtual int __stdcall DoSomething_str  (const char *x) = 0;
};


Conclusion:

Ok, so where are we at? Well, at the start of the article, we talked about why BCB can't call C++ member functions in a DLL if that DLL was compiled with MSVC. The reason is that the two compilers don't agree on how those member functions should be named. We then discussed three (rather unpleasant) work arounds. Each workaround consisted of an MSVC wrapper DLL for the C++ DLL. The wrapper DLL exposed the C++ class using some format that BCB would understand. The first technique was to flatten each member function of the C++ class into a plain C function. The second technique mapped each member function to a member of a COM object. The last technique relied on the fact that virtual functions are dispatched via a lookup table instead of by name. In this strategy, each C++ member function is mapped to a virtual member function of an abstract class.

The downloads section contains the example code for this article. The first download contains the original MSVC C++ DLL that we were trying to work with. This same DLL is used by each of the three techniques. The example for Technique 2 is not ready yet.


Downloads


Downloads for this article
cppdll.zip VC++ 5 DLL project with exported C++ CFoo class
vcdll2tech1.zip Code for technique #1, flattening a class into C functions
vcdll2tech3.zip Code for technique #3, virtual function/abstract base class wrapper


Copyright © 1997-2002 by Harold Howe.
All rights reserved.