How to divide a Delphi project into BPLs properly?
Asked Answered
L

4

14

The company I work for develops a system in Delphi, that contains dozens of exe modules, and each of them is identical to a certain degree if it comes to source code. Sadly, nobody has ever cared about using libraries to put the shared code in. This means that each time there is a bug fix to do in the code all these modules share, a programmer has to make corrections in all of them separately! It always takes so much time...

I decided to find a method to put the shared code into libraries. I considered DLLs and BPLs. In this case BPLs seemed much more programmer-friendly and much less troublesome, especially that the code is used only in our software and only in Delphi.

I put all the code shared by all the exe modules into BPLs and everything seems fine, but there are certain things I don't understand and would be grateful if you explained them to me.

  1. What I expected after dividing the code into BPLs was that it would be enough to deploy exe files with the BPLs I created. But it turned out that they need an rtl100.bpl and vcl100.bpl as well. Why is it so? I want to deploy exes and my BPLs only. I don't want to provide end users with a whole bunch of libraries supplied by Borland and third party companies :). I want them to be compiled within exes as they used to be compiled before. Is it possible to do that?

  2. What I did so far was:

    • I put all shared pas units to BPLs. Each BPL contains units belonging to the same category so it is clear for programmers what code to expect in a given BPL.
    • Each BPL is a "runtime and designtime" library.
    • Each BPL is "rebuilt explicitly". The two latter are default project settings for BPLs.
  3. And if it comes to the exe projects:

    • I deleted all units that I had earlier put to BPLs.
    • I installed my BPLs from the Tools->Install package menu in BDS 2006.
    • In my exe project settings I checked the option "build with runtime packages" and I listed all my BPL packages in the edit box below (only my packages, as I cleared all other ones that appeared there).

This is all I did. The exe projects compile properly, but I have no access to the source code of BPLs (I can't navigate into that code from my exe projects), even though all BPLs are stored together with their source code files. Why? It seems strange to me.

I always tend to write lengthy descriptions - sorry for that :). I will appreciate your help. I just need a few words of explanation to the points I mentioned: deploying exe with my BPLs only, the correctness of what I did as a whole, and the inability to navigate into BPL source codes. Thank you very much in advance!


Thank you all for the discussion. Some said the approach I chose was not a good idea. Our software consists of more than 100 modules (most of them being something like drivers for different devices). Most of them share the same code - in most cases classes. The problem is that those classes are not always put into separate, standalone pas units. I mean that the shared code is often put into units containing code specific to a module. This means that when you fix a bug in a shared class, it is not enough to copy the pas unit it is defined in into all software modules and recompile them. Unfortunately, you have to copy and paste the fixed pieces of code into each module, one by one, into a proper unit and class. This takes a lot of time and this is what I would like to eliminate, choosing a correct approach - please help me.

I thought that using BPLs would be a good solution, but it has some downsides, as some of you mentioned. The worst problem is that if each EXE needs several BPLs, our technical support people will have to know which EXE needs which BPLs and then provide end users with proper files. As long as we don't have a software updater, this will be a great deal for both our technicians and end user. They will certainly get lost and angry :-/.

Also compatibility issues may happen - if one BPL is shared by many EXEs, a modification of one BPL can bee good for one EXE and bad for some other ones - @Warren P.

What should I do then to make bug fixes quicker to make in so many projects? I think of one of the following approaches. If you have better ideas, please let me know.

  • Put shared code into separate and standalone pas units, so when there is a bug fix in one of them, it is enough to copy it to all projects (overwrite the old files) and recompile all of them.

This solution seems to be OK as far as a rearly modified code is concrened. But we also have pas units with general use functions and procedures, which often undrego modifications - we add new functions there whenever necessary, but in single projects. So imagine that you write a new function in one of the 100 modules and put it into its general use unit. After a month or two you modify a different module and you think you need the same function you wrote 2 months ago. You have to find the module (it's difficult if you don't remember which one it was) and copy the function to your code. And obviously - the general use units become completely different in each module as long as they are stored in each project separately. And then, if there is a bug fix to do... the whole story repeats.

  • Create BPLs for all the shared code, but link them into EXEs, so that EXEs are standalone.

For me it seems the best solution now, but there are several cons. If I do a bug fix in a BPL, each programmer will have to update the BPLs on their computer. What if they forget? But still, I think it is a minor problem. If we take care of informing each other about changes, everything should be fine.

  • @CodeInChaos: I don't know if I understood you properly. Do you mean sharing pas files between projects? How to do that? We store source codes in SVN. This means that we would have to store shared code in a separate folder and make all projects search for that code there, right? And download from the SVN a project and all folders it is dependent on...

Please, help me choose a good solution. I just don't want the company to lose much more time and money than necessary on bugfixes just because of a stupid approach to software development.

Thank you very much.

Labile answered 12/8, 2011 at 16:28 Comment(4)
Why is recompiling a few programs so much work? You can obviously share the sourcecode without using any libraries.Seclusive
What do you hope to gain by using packages? It sounds like you think that modules will make things better, but you haven't considered the way that they make things worse. (Fix a bug in baselib1, and it causes a regression in app3,app5, and app7, while fixing a bug in app2)Disserve
Could you please see my yesterday's answer and help me find the best way to modularize the code? I can see that both of you have a different point of view on that. What is your approach to software development that makes it quicker to fix bugs and implement new features in multiple software modules? I will be grateful for your suggestions under my answer. Thanks a lot.Labile
@CodeInChaos: could you please tell me more about that? How to share source code without using libraries? Do you mean sharing single pas units between multiple projects without copying them to each project?Labile
M
13

Even though this question has an accepted answer I'm going to take a stab at it.

The title asks how to divide a project into bpls but the real question appears to be: "What's the best way to share code between projects?"

There are a few ways to do this:

  • Shared units
  • Dlls
  • BPLs

Regardless of which direction you go you will likely need to restructure your projects. From your description it sounds like each project is developed in relative isolation. Code is shared using copy/paste, which quickly gets out of sync and result in a lot of duplicated effort. So lets examine each of the techniques for sharing code.

Shared units


This is the most straightforward approach. You create a shared location and place code you would like to reuse among your projects into this location. The units are statically linked into your projects so you don't need to worry about deploying extra dependencies along with the main executables. Statically linked units are by far the easiest to troubleshoot and debug.

The compiler needs to be able to find your shared units. There are 4 ways to tell the compiler where to look.

  1. Add them to the project - SHIFT+F11 - Adds a reference to the unit into the project files (dpr, dproj). The IDE will normally use relative paths if the unit is located under the same directory tree as the project files, otherwise it will use absolute paths, which can be problematic if developer machines aren't configured identically.
  2. The project's Search Path - CTRL+SHIFT+F11 Delphi Compiler > Search path - Add a directory and the compiler will look there to find units mentioned in the uses clause of any unit in the project. Its best to use relative paths if you can. You can also use environment variables: $(MyPath)
  3. Global Search Path - Tools > Options > Environment Options > Delphi Options > Library - Win32 > Library Path - Any paths listed here are available to all projects on a machine. This is machine dependant
  4. Command line - If you build from a script or build automation tool you can set the search path using the dcc32's -U switch or msbuild's /property:UnitSearchPath= switch.

Options 1 and 2 will be the most useful.

As far as your SVN repository goes you have a few options for organizing the projects and shared units. The simplest would be to place all projects under single trunk along with the shared units:

Projects
    trunk
        ProjectA
        ProjectB
        ProjectC
        Library (shared units)

If for some reason the above structure isn't possible you could try this alternative:

ProjectA
    trunk
        Library (branch of main library)
ProjectB
    trunk
        Library (branch of main library)
ProjectC
    trunk
        Library (branch of main library)
Library
    trunk (main library)

In this configuration changes made to each project's library folder would not be immediately available to the other projects. Each project would need to synchronize changes with the main Library project on a regular basis. A side effect of this is that changes that break other projects will be delayed until the other projects are synchronized. Whether you consider this a good or bad thing depends. On the one hand bugs are easier and cheaper to fix when the code they involve is still fresh in the developer's mind. On the other hand if you don't practice unit testing (which I highly recommend you do) or the code is very fragile or you just have developers prone to making reckless changes you may want to control how frequently those changes get pushed into other projects.

Dlls


Dlls allow you to share code by linking to it at runtime. They expose functions that can be called from a main executable or another dll.

While dlls are always linked at runtime you decide whether they are loaded at application startup or only when needed. Loading at startup is called static loading and in Delphi is accomplished using the external directive. The vast majority of the rtl/vcl classes that wrap system api calls use static loading. Dynamic loading lets to delay the loading of a dll until it is required. This uses the WinAPI functions LoadLibrary and GetProcAddress. A corresponding call to FreeLibrary will unload a dll.

Unfortunately standard dlls limit what kind of datatypes can be passed. If you need to access a dll from non-Delphi projects you will need to limit yourself to using c style data types. If you will only be using a dll with Delphi projects you can safely use Delphi strings and dynamic arrays as well if you use the SharedMem unit in the dll and any projects that use it.

You can safely use object's within the dll without problems but if you want to pass objects between the dll and the application you'll need to extract the object's data and pass it as primitive types and reassemble it into an object on the other end. This is called (de)serialization or marshalling and there are much easier ways to do this than rolling your own.

COM (Component Object Model) is well supported in Delphi but it has a bit of a learning curve. Consuming COM objects is pretty straightforward but designing one will take time if you're not familiar with COM. COM has the advantage that it is language neutral and is supported in the majority of languages targeting the Windows platform (including languages targeting the .NET framework).

Bpls


Bpls (also called simply "packages") are specially formatted dlls that make working with objects a lot easier. Like standard dlls they are linked at runtime and can be statically or dynamically loaded. They are easier to learn and use than COM dlls and provide more seamles integration into your projects than COM. Packages are composed of two parts: the bpl and the dcp. The dcp is like the dcu files generated when you compile a normal unit file except it contains a whole bunch of units in it. Using a class that is compiled in a bpl is as simple as adding the dcp to the project's package list then adding a unit to a uses clause of one of the project's units.

When you deploy the app you'll need to install the bpl as well. As other's have noted you have to include the rtl package at a minimum and most likely the vcl package if you use any forms. There is a way around deploying Borland supplied bpls with your projects. You can create a "mini" rtl package that contains only the units your project need. The difficultly is in determining which units to include.

Summary


From the description you've given creating a library of shared unit files to statically link against may be the most expedient route. I would also suggest trying out a program called Simian. It will help you track down duplicate code in your code base for inclusion in your shared library. It doesn't directly support pascal but it does a decent enough job using the plain text parser with a little tweaking of its configuration.

Also I can't stress enough the value of unit testing. Especially if you're moving toward shared libraries. A suite of well written unit tests run on a frequent basis will give you instant feedback when a developer changes a class and it breaks an unrelated project.

Mandel answered 8/9, 2011 at 17:24 Comment(3)
Thank you very much, I really appreciate your answer as you described each possible approach in detail. If it comes to the method we finally chose, it is the first one, namely shared units added directly to projects. We decided to store these files in the same directories on all programmers' machines.Labile
Let me ask you one more question. I know I should create another thread on that, but I don't think it necessary. I once created a dll in Delphi, that created and returned an object. I had an abstract class declaration of it in my app and the same class in the dll, but non-abstract one. The dll created an object of this class and returned it. Having the class declaration in my app I could use the object easily. And I freed it in my app, not in the dll - I don't know if that was OK. After I had read your answer, I thought this approach might be wrong. What do you think? Thanks for your help.Labile
Yes it is possible. After all an object reference is just a pointer when it comes down to it. There are problems with this approach though. The structure of an object is compiler dependent. You would only be able to do this if your projects use the same version of Delphi. Both the dll and exe need to share the same class definition and can't be updated independently. Freeing an object in the exe that was created in the dll can result in memory corruption. There's a few other problems I'm forgetting. Bottom line - COM or BPLs solve these problems for you.Mandel
S
9

Imagine you have a project with an EXE and two different BPL modules, and somewhere in that codebase, there's a line that says if MyObject is TStringList then DoSomething;. The is operator works by examining the object's class metadata, stored in the VMT, and then following a chain of VMTs through the ClassParent pointer, to see if any of them match the class reference (also a VMT pointer) for TStringList. In order to make sure that this will work correctly, there needs to be one single VMT for TStringList that's the same throughout your entire program, no matter how many BPLs it's divided up into, which means it has to be in its own package. That's why system runtimes like rtl*.bpl and vcl*.bpl are necessary, and there's not much you can do about that. It's part of the price of using BPLs.

As for not being able to debug, you need to make sure that the BPLs are built with debug info enabled and that the debugger knows how to find the folder where the DCP (the file containing the debug info for the BPL) is located. And you won't be able to trace into system BPLs, because debug-enabled DCPs weren't shipped with your version. They were added pretty recently, I think in XE but it might have been in D2010.

Sunil answered 12/8, 2011 at 16:53 Comment(4)
When using dlls you don't need rtl*.bpl, but if you don't watch out you get the problems that Mason explained. Errors like TStringList is no TStringList.Piddling
@Lars That problem is solved by using the same Delphi version for compiling both the BPL library and the EXE. But you don't need to distribute RTL/other BPLs.Compensation
@Compensation unfortionally that is not the case. Mason explains why. Even if both dlls have the same TStringList version, they are still not the same class. The VMT address differs.Piddling
This reminds me of the time when I assisted a team that did a marvelous architecture that - based on some principles from a totally different software development field - divided their app in many DLLs, all (even the business layer DLLs!) partially loading BPLs. It was a nightmare as they all had separate copies of RTL and VCL, with multiple TApplications competing for ownership, classes and interfaces with the same names but different VMTs.Socratic
C
3

Why can't I browse my source code? Is there a way to fix this?

You can not browse the source code of the units included in the packages because they are neither in your project, your library or search path.

configuring your search path

The way I solve this is adding the directories to the project search path. This way the compiler does not know about those files (and does not try to recompile them) but the IDE let's you browse their content and debug into them.

Compensation answered 12/8, 2011 at 17:29 Comment(3)
-1 for your first point, on accuracy grounds. The RTL.bpl is necessary in order to avoid the problem described in my answer. If you don't add that to the list, the compiler will put the dependency in anyway because it needs to be there.Sunil
IIRC having the VLC and RTL linked statically into the EXE you don't need them to be linked dynamically as runtime packages. Obviously RTL is always needed, be it static or loaded at runtime.Compensation
Yeah, here's what will happen. It'll statically link the RTL into the EXE. The EXE starts up, it's got the RTL, it's happy. Then it tries to load the BPL. The BPL needs to bind to the RTL, and that needs to be done by the Windows Loader, before any Delphi code gets ahold of it, so it can't bind to what's in the EXE, so it has to load rtl.bpl. Then the program notices that there's a new RTL and has to rebind stuff, and all sorts of problems ensue. I have done this. I've had to debug this. It's not pretty, and you really can't win. Just make sure everything's explicitly using rtl.bpl.Sunil
A
2

"In my exe project settings I checked the option "build with runtime packages"

That is why you cannot deploy without the BPL's etc - this option is confusing for a lot of developers -"build with runtime packages" means that you will need the bpl's present at runtime. Uncheck that option and the packages will be linked into your exe at compileTime. (Your exe will g-r-o-w in size.) The idea behind the "build with runtime packages" is to keep the size of exe's down and allow several apps to share common bpl's because they are NOT linked into the exe @ compileTime - that's the upside. The downside you are now experiencing - you must distribute your bpl's with your exe.

Aluminous answered 13/8, 2011 at 4:2 Comment(2)
But I tried doing that. And after unchecking this option, my EXE couldn't compile. Why was it so? The compiler couldn't find any of the units stored in my BPLs.Labile
You need to configure the compiler search paths under your project options.Aluminous

© 2022 - 2024 — McMap. All rights reserved.