Illés Péter (Peter Illes)
Delphi 2.0 is the most powerful RAD tool available today. The optimizing compiler produces first class Win32 code. The support for OLE Automation, the even faster scaleable database technology, native support for OLE controls (OCXs), the complete support for the Windows 95 user interface, and advanced features like multi-threading and MAPI, make it the choice of platform for many new and improved applications. Borland did a lot to assure that most of the Delphi 1.0 code simply recompiles. But mainly due to the differences between Win16 and Win32 it takes a little more than just recompiling to port your non-trivial ("legacy") code to
this new platform. And even some sorcery to continue supporting Win16, and maybe MS-DOS for a while (one or two years) with the same code base. In this article we will examine this porting process, pointing out pitfalls that should be avoided and giving guidance on finding the fastest, least painful way to your next release.
Introduction
Delphi was a magnificent success in 1995 due to its combination of an established, easy to learn yet flexible object-oriented programming language (Object Pascal), a world-class Rapid Application Development environment, and its extensive support for Client/Server and database development. Meanwhile we have witnessed the dawn of the Win32 age by Microsoft's introduction of Windows 95. The successor of Windows 3.x is actually the third Win32 platform, but its predecessors (Win32s and Windows NT) could not achieve the critical mass that is necessary to transform the whole industry. The situation right now closely resembles the dawning of the Windows era. Many members of the industry (analysts, journalists, managers, and developers
alike) were skeptical about Windows 3.0 and not willing to jump on the
bandwagon. And in a year or so, most MS-DOS based applications became obsolete,
and many companies lost their leadership positions because they didn't
have a Windows version of their most popular apps. Now the industry is
transforming again and it is time to start working on the Win32 version
of our applications. That's why it is so important for us that Borland
is introducing the second generation of its RAD product line, Delphi 2.0
the most powerful Win32 RAD tool available today.
Many of us have managed transformations like this before (at least from DOS to Windows) and still others will face this challenge the first time. To be successful in the porting process one should go on in an orderly fashion. We'll introduce an organized approach that will result in early success with a robust architecture for future refinements. In order to do so we'll define the starting points, target platforms, porting goals, and stages of porting in Part I. In Part II we're getting into details of general Win16 to Win32, and specific Borland Pascal
and Delphi 1.x to Delphi 2.0 conversion issues. We will also visit critical Win32 concepts (processes, threads, flat memory, etc.) briefly in this part. We won't delve into some areas of porting (notably database issues) and won't touch stage 2 of the porting process and beyond (see Part I for an explanation of stages) in this article due to space constraints. For an extended explanation see our whitepaper, or other references included at the end of the article.
You should be aware of the fact that most porting issues discussed here
are rooted in the changes introduced by Win32. We could treat them in an
abstract sense (independent of a development platform) but decided to show
all issues with a Delphi perspective - so you can put them into practice
right away.
Let's introduce some notations before we go on. DOS stands for any incarnation of the old Disk Operating System used on IBM PC compatibles (MS-DOS, PC-DOS, DR-DOS, etc.). Win16 stands for Windows 3.0, 3.1x, and Windows for Workgroups 3.11. Win32 should denote the new 32 bit API. WinNT stands for Windows NT 3.51 or later and Win95 means Windows 95 build 950 (the August release) or later.
Now, let's start our quest.
Part I: Managing the Porting Process
Where are You Coming From?
Thanks to Borland's 13+ years of success with Pascal, you may come from different environments. Here are the most typical scenarios:
Real mode or Protected mode DOS Turbo Pascal
or Borland Pascal. Yes, there are still some of us out there who will make
the Transition now. You'll have the most work to do, but you may be at
an advantage as you don't have to learn the internals of Win16 and you
can start exploiting the power of Win32 right away. You don't have to face
the steep learning curve generally associated with Windows programming
due to the fact that you are going to use a RAD tool that will hide most
of these details. Many very useful applications can be created without
digging deep into the Windows API. Most of your code may have to be re-written,
but you can save the engine of your apps, and even your user interface
architecture, if you were using an event-based approach (e.g. Turbo Vision).
We were able to port a DOS (Protected mode) app's core with a functioning
user interface (it was a CAD type app!) without architectural alterations
in a month... You'll have to update your object oriented programming knowledge
since it is a norm in Delphi, but you can get along easily with the Delphi
manuals. You also need some good books on Win32 programming. Don't forget
to study and implement user interface design principles, as your UI will
be a major factor in your success or failure in this user-centric world.
And Chapters 19, 20, and 21 (among the others) of Delphi Developer's Guide
from Xavier Pacheco and Steve Teixeira is a must-read for you it
will have some detail on moving from DOS to Delphi that we cannot cover
here due to space limitations. See the References section for sources of
information.
Windows API or Windows OWL Turbo Pascal or
Borland Pascal for Windows. You have an established understanding of the
Windows architecture, and the Win16 APIs, so you can focus on the differences
between Win16 and Win32. With smart approaches you can save most of your
work. The core of another CAD product of ours was ported from BP7/OWL to
Delphi 2.0 in 2 weeks (if we omit the concept development of porting, doing
the RTL70/Objects.Pas port, etc. things that you don't have to repeat).
Of course we have since spent a lot of time fine-tuning and re-implementing
portions that can be done better now. But it did work. You may also find
the Delphi Developer's Guide a valuable tool.
Windows VCL Delphi 1.x. According to Borland,
you have to do the following: Use File|Open to open your project and Project|Compile
to have a 32 bit version. Period. Now the interesting thing is that it's
basically true. If you have kept with Delphi 1.x without reverting to the
Win16 API, assembly and inline programming, depending too much on the size
of variables, etc. you will get a Win32 app right away (except some tiny
changes). And if you did utilize features like that, you're still far ahead
of the rest of us in getting into the 32 bit world most of your code
is Win32 ready. Borland did plan ahead in architecting Delphi 1.x so that
the transition shouldn't be a big effort on your side (they have done most
of the porting for you in Delphi 2.0 and the new VCL).
No matter where you are coming from, you can be assured: you have the easiest job getting into the Win32 world. Delphi 2.0 is a unique opportunity for you.
Where are You Going To, and How Far?
32 bit Windows? Which one? Win95 or Windows NT? Do you want to just have your application run on these platforms, or to take full advantage of their new features?
Where To? The 3 Versions of Win32
Win32 does not denote an environment or operating system, it denotes
an Application Programming Interface (API) specification, implemented by
at least three platforms:
Win32s the extension of Win16 with a minimal subset of the Win32 API. It is a set of DLLs and support files that can be freely distributed by software publishers to support their applications on Win16.
Windows NT the full implementation of the Win32
API on many types of hardware, including x86, Alpha, and MIPS processors.
It also supports multiple processors, that is, it can run "threads" (the
unit of execution in Win32 parlance) simultaneously. It is a completely
new, robust design from the ground up, but it requires hardware that won't
get into mainstream usage for some more time, so many developers won't
target it specifically in the short run.
Windows 95 the mainstream implementation of Win32 for x86 processors, with most of the original Win32 API functionality plus some additions that will be introduced in the next release of Windows NT, too.
Win32s is mostly considered obsolete now with the release of Windows
95. Its limitations are so numerous that it's best to forget it completely
as a possible platform. Also, Delphi 2.0 does not support Win32s development:
the RTL would require adjustments. So you should decide between Windows
NT and Windows 95. Fortunately, in most cases this means that you don't
have to decide at all, since you can have the same binary code running
on both platforms (on x86 architectures). Windows 95 is the mainstream
Win32 implementation, so if you develop consumer-type or (hardware) cost-sensitive
applications, you should target this platform. If you are developing heavy
duty applications, like high-end CAD, (client/)server databases, Internet
server apps, etc., you should choose Windows NT for its robustness and
power.
Another question is: we have to hurry with the porting project, but
how much? By the time of writing, there are only some hundred apps with
the Windows 95 logo, and only some thousand apps that are for Win32 specifically.
And there are tens of thousands of apps that are still for Windows 3.x.
There are 10 million copies of Windows 95 sold, whilst there are 50+ million
copies of Windows 3.x out there. But you should not be confused by these
numbers. The major players no longer develop apps for Win16. 1996 will
be the year of Win32 and you should act right now.
A final word on platform selection: for some time you may have to support your Windows 3.x users (given the numbers above). That means that you will have to manage to keep either a common code base for both Win16 and Win32, or even worse, keep in sync two code bases. We will return to this question later.
How Far? Three Stages of Porting
There are three well separated stages of porting to any platform. In our particular case these are the following:
Stage 1: Run on the platform. This means that you can successfully compile you program with Delphi 2.0 and it runs as expected. The rest of this article will mostly deal with this problem.
Stage 2: Behave as a native application on the platform.
In our case this means Windows 95 logo compliance, including: OLE support,
Registry usage, using the right mouse button for context menus, etc.
Stage 3: Exploit the platform to achieve the best
possible performance. This may include multithreading, overlapped I/O,
Unicode, memory usage optimizations, etc.
Of course, you don't have to precisely follow these stages. Go whichever
way your application's needs lead you. This may mean that you re-design
the data structures of your application at once to take advantage of the
flat memory model, though this activity belongs to stage 3. You most probably
will start using the common dialogs at once (Delphi will does this for
you), though this is a stage 2 process.
Key Elements of Successful Migration
Our experience has shown that both managers and developers have roughly the following considerations when porting applications (some of these considerations apply only if you are not coming from Delphi 1.x if you are, you are in a much better position):
Do it as fast as possible. This is an ever-lasting
request from management. As competition is increasing in the industry,
it is getting more-and-more important to be there before the competitors.
Save as much code as possible. This is the only way to satisfy the above point. By smart decisions you can spare a lot of time and coding. Without porting the RTL70 core we should have had to re-implement most of our old apps. And with minimal trickery around the OWL dispatch mechanism we saved parts of even the user interface code (we did re-create the user interface in Delphi, but were able to save the dispatch mechanism and architecture).
Have code that compiles as early as possible. This
will please both management and the developers. You are dead if you have
to wait 2 to 3 months (or more) for a complete rewrite before you can start
compiling. Instead, compile with the necessary fixes in 3 or 4 days, then
start building the skeleton anew in Delphi (while your whole app is still
compiling!) and add the converted engines, dialogs, etc. step-by-step.
You can compile - run - test - debug - compile-... whenever you want, to
find design problems, etc. This approach will help you greatly. You will
be able to split the conversion among many developers and test the new
parts as soon as they are ready.
Have backward compatibility with versions of apps on older platforms (e.g. DOS, Win16). You have DOS and Windows 3.x apps. How do you make bug fixes in two (or three) different code bases? Have one engine code for all the target platforms. Engines typically don't exploit the features of an environment. So find the least common denominator (this may mean Delphi 1.x features, as it can compile DOS code too) and use it in your code wherever possible.
Become as fully compliant with the new environment as possible. You won't want to support OWL any longer, since Borland discontinued it for the Delphi product line. VCL is the future: Borland will continually add new features as they have demonstrated it with Delphi 2.0 (enhanced OLE, better database support, etc.). You'll definitely be in a better position to achieve a Stage 2 or 3 port (see the next section for an explanation
of stages) with the support of the development environment. On the other hand you may be forced to use a mixed object model, as replacing old type objects with new ones is a difficult and error prone process if you have a more than trivial object architecture. Fortunately, Delphi 2.0 has a solid implementation of the old type objects.
We will have detailed discussions about achieving the first three points,
and will touch the latter two to a certain extent.
Part II: Porting Issues
Overview
We recommend reading this part through and then formulating a porting checklist (plan) that may look like this (assuming that you first want a released Win32 app, then want the Win95 logo; there are things that do not apply depending on where you are coming from):
Make a backup copy of your code! Get the code to compile (in this phase comment out difficult to port code like assembly, inline, and platform specific parts create stubs) - in this phase you should use a tool
like DelphiPort™ Expert to lighten your load.
Fix Windows API changes
Eliminate obsolete Delphi/BP7 routines
Replace Objects.Pas and the OWL units (DOS/OWL)
Get the code to run (fix errors in the main portion
of your app) - DelphiPort™ Expert will also help here to pinpoint some
issues that may cause problems. Re-create the main window in Delphi
(DOS/OWL) Fix 16 bit pointer manipulations (Seg, Ofs, etc.)
32 bit data structure fixes
(casting with Word instead of THandle, etc.)
String and integer issues
Assure basic operation & fix release.
Re-implement commented-out
code
Long filenames
File compatibility (packed
records, integers, etc.)
Fix architectural changes
(e.g. data sharing between apps, asynchronous input model)
Re-build and fine-tune the
whole user interface in Delphi
Turn on Hints & Warnings
in Delphi and check all messages: many will lead to long
lurking bugs, many to porting
problems
Add new features
Get the Win95 logo
Fine-tune for Win32
During conversion it's a good idea to put in some informative comments. Some of my favorites:
{!PORT!} the commented out part must be ported
later
{!FIX!} the code will have to be fixed later
(temporary solution)
{!TEST!} this part should be thoroughly tested
{!FYI!:...} for your information; the comment
explains decisions that will limit
performance, usability, or other characteristics
(handy to explain that the
implementation is limited in Win16 only and will
do magic under Win32)
{D!HACK} dirty hack; something that has a
good chance to stay in the code for quite
some cycles, but is against all norms... (a confessed
sin)
Use the WIN32 symbol in conditional compilation directives to create Win32 branches. WINDOWS is not defined in Delphi 2.0 by default, so you can use it to identify Win16 code. Do something like this:
{$IFDEF Win32}
Win32 code comes here ...
{$ELSE}
{$IFDEF Windows}
Win16 code comes here ...
{$ELSE}
{$IFDEF DPMI}
DOS protected mode code comes here ...
{$ELSE}
DOS real mode code comes here ...
{$ENDIF}
{$ENDIF}
{$ENDIF}
The following sections contain background information, tips, and samples to achieve the above steps as easily as possible.
This symbol will indicate issues that are specific to Delphi 2.0. The rest is true for all development platforms.
Top Issues
In this section we'll overview the most important issues. These issues
are detailed in later sections (with many others). As you will see most
of these issues are caused by the changes introduced with Win32 and are
not a specialty of Delphi 2.0. Actually, Borland did hide most of the changes,
so porting VCL code is really easy. Here we are:
Memory segmentation is gone. You can create data structures of any size, but you will have to fix code that used to manipulate huge data structures (larger than 64 kB). In other cases, memory architecture changes won't cause much headache for you as they are carefully and cleverly wrapped in the familiar RTL functions.
Many 16-bit types grew to 32 bits, including integers,
handles, graphics coordinates, and wParam. This may cause file incompatibility
problems and type conversion bugs.
Many Windows API functions have changed or have been replaced by others. Use a utility that finds them all and tells you what to do.
The new asynchronous input model of Win32 may cause problems to your old mouse capturing logic.
Long filenames are easier to implement than you would expect. Just make sure you're using the new long String type and the RTL filename manipulation routines. The new common dialogs will be used by VCL automatically.
You will have to fix your code that does data sharing
between applications and already-running-instance determination, as these
aspects of the Win32 API have changed drastically.
The new long String type will not cause much problem if you look out for some bits discussed later. And yes, you are no longer limited to 255 characters in a string!
DLL initialization and finalization has changed in Win32. You will also have to look out for your calling conventions.
OWL is gone, but it's easy to replace with VCL. You may save your old code that uses the old RTL70 streams and collections with third-party implementations.
Assembly code should be fixed,
or rather eliminated, as Delphi's new optimizing compiler produces world-class
code. Inline code must be re-implemented as it is gone.
There are some minimal changes in the VCL and RTL that will need fixes. VBXes will have to be replaced with OCXes and 16 bit third-party DCUs/DLLs must be upgraded to their 32 bit versions. VBXes are gone since Microsoft does not support them in Win32.
Windows Architectural Issues
Processes and Threads
The term process identifies an instance of a running program. It owns a 4 GByte address space and certain other resources such as threads, that are the atomic unit of execution. The process itself is not running, it is static. Your application starts execution because the system creates the primary thread after it successfully loaded your app (the process). This thread will enter at the Begin statement of your program. One process may create and own more than one thread. These threads are scheduled to execute by the operating system. Under WinNT, if you have more than one CPU in your machine, threads (as many as the number of CPUs) can
execute simultaneously. In other cases (fewer CPUs than threads, one
CPU, or under Win95) the threads are scheduled preemptively. The scheduling
is based on thread priority (this can be set by API calls; the system changes
it dynamically to guarantee the execution of low priority processes). When
the last thread (not necessarily the primary thread) stops, the process
is terminated by the operating system. At this point all resources owned
by the process will be automatically freed.
When you start using multiple threads in your Delphi application, you
will want to synchronize them, otherwise corruption of data will/may occur.
The use of will/may is intentional: in theory it is only 'may', but it
will generally lead to very difficult-to-figure-out bugs (non-deterministic
bugs that depend on the will of the thread scheduler of the OS to show
up or not), so it's better to imply that if you have multiple threads without
proper synchronization, corruption will occur (the question is not whether
it will, but when and how: not in all cases, and not the same way!).
Memory and Pointers
In Win32 each application has a separate 4 Gigabyte linear virtual address space. Linear means that you can address the whole addressable range with a 32 bit offset between 0 and 4,294,967,295 (232-1) without worrying about crossing 64 kB segment boundaries. Segments are gone. This also means, that you are no longer limited to 64k on a single data structure: you can easily create multi-megabyte structures and manipulate them with one pointer, without hacking with AHIncr, Seg, Ofs, DSeg, CSeg, SSeg, SPtr, and the like. You will have to review your code and rewrite all routines that made use of 16 bit pointer operations like the ones above. Your new code will be much simpler. Since few systems have 4 GB RAM (for each process!) installed, the operating system is using address mapping to translate a subset of each process's virtual addresses into physical storage. That is, only portions of the whole 4 GB address space are stored in physical storage
at any given time. The system (generally with CPU hardware support) uses private lookup tables to convert virtual addresses to physical memory addresses. This means that there are 'holes' in the address space of your application areas that are not mapped to physical storage. One advantage of mapping is that the memory manager can easily relocate code or data in the physical memory without the need to modify the code of any application (it only updates its tables). It is also possible to optimize physical storage usage this way: for example the same copy of the operating system will be mapped to each process's address space (see Figure 1
later).
The term physical storage was used intentionally in the above paragraph instead of RAM. In order to run multiple and demanding applications, modern operating systems use page swapping to simulate RAM with hard disk swapfiles. When the CPU is to access code or data that is not in the system RAM, then the OS will bring it in from the swapfile, making room for it by pushing out pages of RAM that are currently not in use. So the total physical storage available consists of the RAM installed in your system (this is fixed) and the size of the
hard disk swapfile(s), that can vary dynamically depending on how much
free space you have on your hard drive and as the load of the system dictates.
For example: although you have only 16 MB of RAM, your system may have
100 MB of physical storage at a given time (the remainig 84 MB comes from
the hard disk swapfile). Of course, if you have more RAM you will have
better performance, as the system will have to turn to the hard disk swapfiles
less frequently to re-load portions of the physical storage (a very time
consuming operation).
The concept of virtual memory has already been used in Win16 (in enhanced
mode). What's different is that in Win16 all programs shared the same view
of the memory, while in Win32 each process has its own private 4 GB address
space. This is a major plus for robustness: applications cannot access
each others' data structures, thus cannot corrupt them. The problem is
that you cannot share data between apps the old way by passing pointers,
since one process's pointer has absolutely and definitely no meaning in
another process's address space (it may point to a different data structure,
or nowhere at all).
The 4 GB address space contains your code, data, stack, heap, all DLLs,
resources, and the operating system itself. Again, you don't have separate
segments. The address space is partitioned as follows:
Figure 1 - Memory Layout
Under WinNT the upper 2 GB is reserved for the OS. It is inaccessible (and generates an access violation), thus the system is protected from tampering with. Under Win95 the top 1 GB contains the OS core and (unfortunately) is not protected, so a "badly behaved" application can corrupt the OS data. The 1 GB above $8000,0000 (i.e. the third GB) is shared among applications under Win95 (as is the OS core). The lower half of the address space is the process's private area. Some sections are still reserved. Under WinNT there are two 64 kB
guard blocks at the top and the bottom. Under Win95 the lowest 4 MB is reserved for MS-DOS and Win16 compatibility (don't touch it), out of which the lowest 4 kB is a NIL pointer guard block. The guard blocks will generate access violations (EAccessViolation exception) if accessed: this comes in handy to catch NIL or invalid pointers. Under WinNT, memory mapped files and Win32 DLLs (kernel32, user32, etc.) will be mapped into the lower 2 GB.
Keep in mind that the system is more reluctant to identify "wandering" pointers (in the same process) than it was in Win16 (or 16 bit DPMI), because there crossing the segment limits caused General Protection Faults (Runtime Error 216 this helped to find quite some bugs). There is no such thing under Win32 (it is possible to protect pages (4 kB blocks in most implementations) of memory, though). You will get access violations (EAccessViolation) whenever you access an area marked as protected (see the discussion above) or an area that is not assigned (mapped) to physical storage at all. You should be even more careful to make sure
that each pointer stays within its intended bounds.
The notations "global" and "local" memory no longer apply. All memory is equal and is in your 4 GB address space. The GlobalXX and LocalXX Windows APIs are kept for compatibility, but they work on the same memory.
In Win32 (if you are working with the Windows API), to use portions of the 4 GB address space, you must first reserve a region of memory, and then commit physical storage to it (both can be done with the VirtualAlloc function even in one step). Reserving a region means that you tell the system to set apart a specific portion of your address space for you (you can even tell where this area should be). This is a fast operation, as there is no physical storage assignment. Then, when you want to use this region, you commit physical storage to it (or to a
portion of it you can commit storage in steps). You can reserve and commit memory in pages (4 kB in most implementations).
The Delphi memory manager is still doing the sub-allocation scheme for optimum performance (to allow you to allocate smaller chunks of memory than a page and to speed up things by not going to the OS with every request), but it has been refined for the Win32 environment. It is smart enough to unify memory management of different modules in the same process (if you use the ShareMem unit). This means that your application and your Delphi DLLs will share the same memory manager so it won't cause a problem if one module (e.g. a DLL)
frees a data structure on the heap that was owned (allocated) by another module (this is important, as the new long String type is automatically managed on the heap).You can generally use the Delphi routines since they can handle any size data. You may want to revert to the Win32 API to achieve special effects, like sparse matrices (hold a 500 MB matrix in actually 4 kB...).
Under Win16, memory allocation routines returned pointers with zero
offsets. This is no longer the case, so code that depends on this behavior
has to be revised.
Another important issue is structure alignment. Delphi 2.0 will align
data members of structures (records, objects, etc.) at addresses that are
"natural" for the type of the data member by default (you can turn off
this behavior, but a slight loss of performance will occur). It is done
to improve performance with current processor technology. Aligned structures
will generally be larger than their non-aligned counterparts. This may
break code
that depends on the fact that data members are packed. For an example,
look at the following fragment:
Type
T1 = Record
b: Byte;
w: Word;
End;
T2 = Record
b: Byte;
li: LongInt;
End;
Under Win16, SizeOf(T1) would be 3 and SizeOf(T2) would be 5. Under
Win32, they are 4 (+1 byte after b to pad w to an even word boundary) and
8 (+3 bytes after b to pad li on an even dword boundary), respectively.
So use the Packed keyword on all records where you depend on tightly aligned
fields (e.g. old structures that you read in from a file), or even better,
fix the code to cope with the change.
As we have discussed, it is not trivial how much free physical storage is in your system (it is a function of the size of the system paging file that can change its size dynamically). That's why MemAvail and MaxAvail have been removed. You can use the new GetHeapStatus function instead.
In Win16 you had a limitation of 64 kB for the data segment, stack,
and local heap together. In Win32 there is no such limit. Your static and
dynamic data can be as large as the system will handle, and Delphi manages
the stacks (one for each thread) so that they can grow to 1 MB each automatically.
Welcome to 32 bit world!
Asynchronous Input Model
An important issue for many applications raises from Win32's departure from the synchronous input model of Win16. In Win16, there was a linear path of execution with cooperative multitasking, so message processing was synchronized. In Win32, the goal of robustness requires that one hung thread should not bring down the entire system, so each thread has its own message queue, and the messages in these queues are processed asynchronously. Operations like SetFocus(), SetActiveWindow(), or SetCapture(), work on per thread basis, i.e. you can only operate on those windows that are created by the current thread, so a malfunction in one
application does not hang other applications. Also, the GetXxx counterparts of the above functions can return NIL (you should check for it!) to indicate that the thread does not own the queried state (e.g. it does not have the input focus), even if a previous SetXxx call succeeded.
Mouse handling is even more complicated. The mouse shape is managed on a per thread basis, i.e. if you set the mouse to a given shape, then it is moved to a window of another thread, it will be set to the other thread's requirements, and when it is moved back over a window owned by your thread, it will be automatically reset to the shape you requested. Mouse capture is also different. SetCapture() guarantees a system-wide capturing of mouse events while a mouse button is down only. If no mouse button is down, your capture window will receive mouse events only while the mouse is over a window created by your thread (thread-local level). This
indicates that old code that relies on capturing mouse events while a mouse button is not down may be malfunctioning under Win32.
All in one, you should thoroughly test your mouse capture logic and other related issues. A detailed explanation of these problems can be found in Richter's Advanced Windows.
Windows General Issues
Windows API Changes
Win32 is designed so that porting from Win16 should not be too difficult.
Still, there are many changes in the API. Here's a brief listing of these
changes. To get into more details (with a listing of all affected functions
and messages), consult "Porting 16-Bit Windows-Based Applications to Win32"
by Randy Kath on the MSDN Development Library or check out the Win32 API
Help on "Porting 16- bit Code to 32-bit Windows" (included with Delphi
2.0).
Handles became 32 bit (THandle, etc.). You should check your code for places where you have used variables of type Word or typecasts to Word instead of the handle type, since this is no longer valid. The Get/SetWindowWord and Get/SetClassWord functions and GWW_xxx, GCW_xxx constants should be replaced with Get/SetWindowLong, Get/SetClassLong, GWL_xxx, and GCL_xxx for handle access. This change necessitated modifications to all messages that contained handles.
The Windows API Boolean type is 32 bits now. Use BOOL (or LongBool) wherever this may be
of concern.
Graphics coordinates have also become 32 bit. In
Win16 many API calls returned coordinates in the LoWord and HiWord of a
LongInt return value. Now these functions take either a Var parameter of
type TPoint or TSize (they have grown from 32 bits to 64 bits) or a pointer
to such a parameter (if it is valid to pass NIL in order to ignore the
returned coordinates) to return these coordinates, and have a BOOL return
value that indicates success or failure. These functions have been renamed
generally by appending 'Ex' to the end of the original function name (e.g.
MoveTo became MoveToEx). Please note, that though the storage space is
32 bits on all Win32 platforms, current versions of Win32s and Win95 use
only the lower 16 bits. This may lead to problems with negative coordinates
on these systems (for example, coordinate 65535 on WinNT will show up as
coordinate -1 on Win95!). See Lou Grinzo's Zen of Windows 95 Programming
for many hard-to-figure-out details like this.
On the other hand, mouse screen positions (in messages) remained 16 bit integers (SmallInt). So you will have to use the TSmallPoint type, and the SmallPointToPoint and PointToSmallPoint functions to convert between the two types. If you are not using Delphi's event handlers for such messages (that will crack the message for you) you should be aware of a possible pitfall:
Integer( LoWord(lParam) ) {this will not work correctly
- BAD!!!}
Integer( SmallInt( LoWord(lParam) ) ) {this is OK}
The first version will loose the sign of the coordinate (it can be negative) because integers are now 32 bits and can represent values above 32767. The second version works fine on both Delphi 1.x and 2.0.
wParam became 32 bit, so check your message handlers
for declarations of wParam as Word, and replace it with WPARAM (like this:
wParam: WPARAM it will work!). Many messages had to be rearranged
to accommodate the growth of handles to 32 bits. Now the handle itself
occupies lParam, so the other 16 bit part of the lParam had to move to
the HiWord of wParam. You should check all your WM_ message handlers, especially
WM_COMMAND. This is not the case if you were using event handlers (like
OnMouseMove) or message handlers with the Borland-defined message types
(e.g. TWMCommand) as these crack the messages properly. There are other
messages that have been re-arranged, like EM_GETSEL.
The WM_CTLCOLOR message has been replaced with a collection of messages (WM_CTLCOLORBTN, WM_CTLCOLORDLG, WM_CTLCOLOREDIT, etc.) since it had two handles, and each growing to 32 bits, there was no space left for the control type.
Many functions became obsolete and have been removed
either because they were MS-DOS or hardware specific (e.g. GlobalDOSAlloc,
AllocSelector). These changes were done to facilitate the portability of
the Win32 API. Some other functions have been removed due to the architectural
changes form Win16 to Win32 (e.g. GetModuleUsage).
Some functions have been modified to make them more robust (e.g. DlgDirSelect).
There are functions that exist for compatibility
only. These functions are no-ops (do nothing). You may remove them to make
the code cleaner or leave them in your code to preserve compatibility with
Win16. Such a function is MakeProcInstance that will simply return the
value of its input parameter (there is no need for instance thunks in Win32:
all functions can be called directly).
Communications functions have been completely changed, with a standard file-based API in Win32. Now you use CreateFile with a filename of 'COM1' to open a com port, ReadFile and WriteFile to handle I/O, and CloseHandle to close it.
Most of the (low level) sound API has been discarded except the multimedia PlaySound function. You will have to rewrite your code to use wave files or resources with PlaySound.
The Dos3Call function has been removed. It was used
(in Asm blocks) for calling MS-DOS Int 21h advanced file I/O operations.
Win32 introduces a more robust, platform independent file I/O API. This
may affect your code, though many affected calls were already implemented
since BP7 in the Pascal RTL and they are updated for Win32.
If your program did its own customized icon painting (e.g. in response of the WM_PAINTICON message), then you should remove any such code, as Win95 does not support icon painting: the Taskbar displays 16x16 static icons only.
Long Filenames and File Compatibility
An important issue under Win32 is the change in file naming. The user
is no longer restricted to the old 8.3 format (8 characters name, 3 characters
extension), rather he/she can create filenames (and directory names) up
to 255 characters long. There may be multiple periods ('.') in a filename
(along with characters like '~!@#$%^&()_+={}[];'). This means that
you will have to parse for the last period instead of relying on the 8.3
scheme or looking for the first period. Even better, use standard functions
to parse your paths (ExtractFileName , ExtractFileExt, ChangeFileExt, etc.).
When comparing filenames, accommodate for the fact that they preserve case
from now on (but the OS is case-insensitive). The whole path never can
be longer than MAX_PATH (that is 260 characters in current implementations
of Win95 and WinNT). Another limitation is that the registry can record
associations with 3 character file extensions only.
Most of the general Win32 books will tell you to allocate space for MAX_PATH characters to support long filenames. The problem with this solution is that in next generation operating systems the maximum length may change. Another solution is to use GetVolumeInformation to check the maximum length at runtime (Win32 supports multiple file systems concurrently, so it is possible, that your drive A is FAT, drive C is VFAT (Win95 long filename-enabled FAT), drive D is HPFS (from OS/2), and drive E is NTFS (WinNT 'native' file system) with different limitations on filenames...). Fortunately, you're in Delphi, so you can simply use the new
huge String type for filenames. It will accommodate any length. Please see the section on strings for possible pitfalls. You will still have to check for the maximum length when passing filenames to the Win32 API occasionally.
Most of the long filename issues are hidden from you if you are using the common dialogs. You will still have to review your code for String[13]s and other limitations and turn them into Strings.
Another issue is file compatibility, i.e. that you read in the files of the older (Win16, DOS)
versions of your applications. Here are some quick tips to get you started:
Look out for types that have grown for example, replace Integers with SmallInts in records you read in from binary files (you may alternatively modify the reading code to read the old size record with SmallInts, and then convert to the new size record with Integers).
Compensate for the differences
between old and new strings (read into an old string, and then assign the
result to a new type string).
Make all records you read in from a file packed to prevent compatibility problems caused by field alignment.
You may need a port of the RTL70 TStream and TCollection (TOStream and TOCollection in our parlance) if you made use of them. If you want bi-directional compatibility (the files that your new Win32 app writes must be read by your old apps), make sure to modify your new writing logic accordingly.
Be warned that replacing Integers with SmallInts and creating misaligned
records with the packed keyword may have a slight performance penalty,
so consider to hide these restrictions into your file i/o logic.
The procedures BlockRead and BlockWrite have changed to allow the read/write of blocks that are larger than 64 kB. The Count parameter and the optional Result parameter (number of records successfully read/written) are now Integer.
Sharing Data between Applications
In Win16 you were able to share data (including GDI objects) between applications through DLLs or by passing handles or pointers.
In Win32, DLLs are not shared, they are loaded into the private address space of each
application. You can't use global variables in a DLL that is used by
two applications to pass
information between the applications.
You neither can share memory by allocating it with the GMEM_DDESHARE
or GMEM_SHARE flags as the returned pointer has meaning only in the address
space of the allocating application, it cannot be accessed by any other
process. Passing GDI handles (of bitmaps, for example) is also an error,
as these handles are valid in the creating process's context only.
To share information between processes, you should create a named memory mapped section (CreateFileMapping) in one app, and open that named memory map in the other app (OpenFileMapping). Note that you don't have to have a file, you can use these routines to create shared memory areas. Another method would be to create a DLL that has a shared data area (DATA SINGLE), but Delphi 2.0 does not support this feature currently.
An alternate way of sharing data is to send the new WM_COPYDATA message
to one of the other processes' windows. SendMessage will automatically
copy the data of the message from the sending process's address space to
the receiving process's address space. This message cannot be posted. The
data being sent must not be modified (by another thread of the sending
process) before SendMessage returns. This message also works between Win16
and Win32 applications.
Finding Other Instances of the Same Application
In Win16 applications were able to determine whether another copy of
the application was already running by examining the hPrevInst variable
(System unit) at startup. In Win32 this variable is always 0, because the
instance handle of a process is the load address of the process, and it
has no meaning outside this address space (thus it is very likely that
many running applications will have the same hInstance). An alternative
approach is to use FindWindow to determine whether any other copy of an
application is running.
Another interesting difference between Win16 and Win32 is that in Win32
the instance handle and the module handle are interchangeable and have
the same value (to be more precise, it is the module handle that is passed
to the application in hInstance), while in Win16 they are quite different:
the module handle was an 'index' into the module database and had the same
value for each copy of a given EXE or DLL, while the instance handle was
indeed the task's default data segment and as such had a unique value for
each copy of an EXE or DLL.
DLLs
Existing Win16 DLLs won't work readily with Win32 applications. To use existing 16 bit code, you should use thunking or an IPC (inter-process communication) scheme like DDE or OLE. All these solutions are rather complicated so it is advisable to convert the 16 bit code to 32 bits right away. For thunking you will need to use the Microsoft thunking compiler and DLLs (in the Win32 SDK).
If you port your Win16 DLL to a Win32 DLL, use the following checklist:
The Win32 DLL entry point (the Begin...End. part
of the DLL) is called with every process attach and detach while in Win16
it is called only once.
You should make your DLL "thread safe" as it can
be called from multiple threads preemptively or functions can be reentered
that may corrupt data without synchronization of access to critical resources.
You cannot share data between apps with DLLs the
old way, as each Win32 process gets its own copy of the Win32 DLL's data.
The export directive is no longer necessary because of the flat memory model of Win32. Put the ShareMem unit first into your Uses clause both in your DLL and in your application if you want to pass new long strings between the two.
Now the details.
To handle the DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, and DLL_PROCESS_DETACH events in a Delphi32 DLL, you have to do something like this:
Library SkeletalDLL;
Uses
Windows;
Procedure DLLEntryPoint( dwReason: DWord );
Begin
Case dwReason Of
DLL_PROCESS_ATTACH: { Handle process attach.
This occurs for each process that loads the DLL.
Put initialization code here. } ;
DLL_THREAD_ATTACH: { Handle thread attach.
This occurs for each newly created thread in a
process that has already loaded the DLL. It won't be sent
for threads that existed before the DLL was loaded. } ;
DLL_THREAD_DETACH: { Handle thread detach.
This occurs for each thread that exits in a
process that has already loaded the DLL. } ;
DLL_PROCESS_DETACH: { Handle process detach.
This occurs for each process that cleanly unloads the DLL.
Put your old ExitProc code here. } ;
End;
End;
Begin { _InitDll calls DllProc if it is not NIL, that is, except the first time. }
If DllProc = NIL Then Begin
DllProc := @DLLEntryPoint;
DLLEntryPoint(DLL_PROCESS_ATTACH);
End;
End.
The Begin statement calls an internal routine called _InitDLL in SYSTEM.PAS. _InitDll sets IsLibrary to True so you can easily test if you are in a DLL (no more PrefixSeg <> 0). DllProc is a NIL pointer by default.
Under Win16, with cooperative multitasking, there were well-defined points in your application where it could lose the attention of the processor, so you did not have to care about preemption and reentrancy, they didn't happen. Under Win32 applications can be multithreaded. This means that you should build your DLL as multithreaded to support preemption and reentrancy. The runtime library (RTL) of Delphi 2.0 is thread-safe, so the open question remains to guard global data- structures against corruption. A possible scenario of corruption
can be the following: Thread A starts modifying a global record that includes an index value (integer) and a corresponding string. First it writes the index value. Then Thread A is preempted, and Thread B is scheduled by the operating system (there is no guarantee where your code will be preempted!). Thread B's task is to copy the global data structure to another place. It reads the index, and then reads the string, but that string does not belong to the index, as Thread A was preempted before it could write the string. So Thread B has a corrupted
data structure. The solution for such problems is to use synchronization objects like mutexes and critical sections. Your threads will also be preempted by threads of other processes. This leads to the same preemption and reentrancy problems as above, if you do data sharing among the processes.
The "DS != SS" issues common to 16-bit DLLs no longer apply. A Win32
DLL is mapped into the linear address space of each process that attaches
to it (loads it) and there is no segmentation of this address space. All
DLL functions and procedures are called using the calling thread's stack
and all pointers are 32-bit linear addresses.
Delphi 2.0 currently does not support shared data segments in DLLs.
You should import DLL functions by name under Win32
as opposed to importing by index, since ordinals may change from version
to version. Exported names are case sensitive (spell them correctly in
import libraries and with GetProcAddress). You must supply the extension
when you statically import to Delphi 2.0 (you will get along without supplying
the extension as far as you run your 32 bit app under Win95, but WinNT
will not find your DLL if you don't supply the extension), and you must
not in Delphi 1.x. This is a Win16 loader bug. It appends .DLL to a static
import module name. To prevent $IFDEFs on each line, put the DLL name in
a string constant like this:
Const
DllName = {$IFDEF Win32} 'MyDll.DLL' {$ELSE} 'MyDll' {$ENDIF}
;
Keep in mind that though it's absolutely legal to pass a pointer to a DLL, this pointer has meaning only in the address space of the calling process, so the DLL should not pass it off to a second application (e.g. with a SendMessage).
Keep in sync your calling convention declarations in your DLL and in
its import unit. The standard calling convention for Win32 DLLs is StdCall,
and you have to specify this in your program when you declare and import
the DLL function. Failing to synchronize calling conventions will cause
access violations.
The address of a function imported statically will differ from the address of the same function if imported dynamically (with GetProcAddress), because static imports go through a jump table which makes loading executable code a lot faster in 32 bit apps than the rewrite-the-code-segments 16 bit process.
The startup time of an application and overall performance can be boosted
by assuring that all DLLs have different load addresses (this will make
the relocation of code unnecessary).