Search:
Locator+ Code:
FTP Home   VSM Home   Product Catalog   Archives   Customer Service   Site Map
 
   Compiler Drill-Down


Take Control of the Compile Process

Take an in-depth look at how VB's compiler works, view your app's object source, and link in C or assembly.

by John Chamberlain

  Visual Basic is a high-level language, but it uses a compiler to generate native code. This gives you a couple of important benefits, including better performance and protection for your source code. But it also means you have less control over how your binaries are compiled, which can be frustrating, especially when you want to hold down the number of files you distribute with your apps.

What you need:
Visual Basic 5.0 or 6.0
C compiler
Debugger
Assembler
All things considered, it's a good trade, but it's possible to improve the terms of this trade by creating an add-in that seizes control from VB and hands it to your applications instead. The Compile Controller add-in enables you to intercept VB's native code generation, then compile selectively and link custom object modules into your VB projects.

This technique comes with an important caveat: Microsoft's policy is that VB is a closed environment, which precludes giving you access to its compiler. You can acquire this access on your own if you possess the right tools, but doing so requires taking a walk on the unsupported side. The right tools include VB5/6, a debugger, and an assembler. I created the code examples using Visual C++ 6.0, which ships with an integrated debugger, and Microsoft Macro Assembler 6.11 (MASM), which Microsoft gives away for free. You don't have to use exactly the same tools I use, but you might need to make minor adjustments to the code depending on the tools you use.

The Compile Controller add-in allows you to delay the execution of the VB compiler and linker as needed and edit their arguments. This makes it possible to add inline assembly to your VB code and incorporate object modules written in C/C++ (or other languages) directly into your project without compiling them as separate DLLs. In other words, you can statically link outside modules, gaining C++- or assembly-level power for critical sections in your apps, yet avoid the hassles commonly associated with shipping additional DLLs. There are limitations—you still can't ditch the VB runtime or incorporate VB's runtime or a subset of it directly into your apps—but you can add any kind of code resource within the overall framework of a VB application. A side benefit of using the add-in and reading this article is that you will learn a great deal about how VB's compiler works.

Be forewarned: The add-in intercepts an internal API call of the VB environment—you can be certain the folks in Redmond don't smile upon this sort of thing. You can safely use the add-in to view the assembly source code of your projects, but you can count on seeing a lot of General Protection Faults (GPFs) if you use the add-in to start inserting your own C or assembly code in a VB binary. The add-in works only with VB5/6 and won't be forward-compatible with the next major version of VB without modifications. You need to consider the power vs. danger trade-off carefully, as you do with any advanced technique.

 
Figure 1. How VB Makes an EXE. Click here.

How the Compile Controller Works
Let's begin by examining the way VB compiles your apps (see Figure 1). Two separate EXEs, C2.exe and Link.exe, perform VB's compilation and linking. C2 is an older version of Microsoft's second-pass C compiler; Microsoft modified it specifically for use with VB. VB activates these EXEs with the Win32 kernel function, CreateProcess, and the add-in hooks this call with a special function called HookDLLCall. Hooking this call enables you to intercept and modify the commands to C2 and Link (see the sidebar, "HookDLLCall: Where the Magic Happens").

The add-in simplifies the process of hooking this call by creating a menu choice on the VB IDE that lets you hook or unhook the call to CreateProcess. Hooking the call and clicking on "Make…" pops up a dialog box that displays the command-line arguments to the executable in an edit box, as well as controls to manage the compile (see Figure 2). You can then edit the commands and substitute alternative object modules before letting the compile process continue.

 
Figure 2. The Compile Controller in Action. Click here.

Visual Basic uses the same pattern to make any native code executable. Making an executable with the Compile Controller activated shows that VB calls C2 once for every file in your project (see Table 1 for a typical call). Next, VB generates an object module for the project itself, then calls Link once at the end. It is Link that writes the project's executable. The compiler takes the VB module—modMath.bas in the example—and compiles it using information in special intermediate libraries that VB deposits in your default Temp directory. After the object code modules have been compiled, VB calls Link to combine them into an executable. VB then deletes the intermediate libraries and object code modules.

The key compiler switch is "-FAs," which lets you generate assembly listings. The add-in's interface includes a handy Generate Listing checkbox that inserts this switch for you automatically. For example, clicking on this checkbox inserts a string like this in the C2 command line:

-FAs -Fa"D:\Example\modMath.lst"

This switch tells the compiler to generate an assembly listing of the object file with source code in a file called modMath.lst. You can read a listing to see exactly how VB compiles each line of your source code. Access to the listing gives you complete control over your project's executable because you can modify and reassemble the listing into custom object code.

 
Figure 3. Take Control of the Compiler. Click here.

You can use different approaches to navigate a build with the Compile Controller, depending on what you want to achieve (see Figure 3). The safest and simplest approach is to generate source listings of your project for research or reference. A more adventurous use is to experiment with the command-line switches of the linker. For example, you might use the /EXPORT or /DEF switches to export functions out of a Basic module and create a normal, non-ActiveX DLL. The most advanced use is to substitute object modules you create for those VB compiles when it calls C2.

Make a Custom Object Module
Creating an alternative object module requires either modifying the source listing of the target module and reassembling it, or employing any programming language to create a new object module and inserting a stub for it in your project. A stub is a basic module with the same interfaces as your externally generated module. Your stub sits in for the external module until link time, when you manually substitute the external object module for the stub's compiled object module. The linker is happy as long as the symbols of the stub and the foreign module match exactly.

The ability to include object modules generated with other languages enables you to write hybrid programs that exploit the relative advantages of each language without the performance hit and additional distributable required when using DLLs. Linking alien object code into a VB project might sound insanely difficult, but it proves easier to use than inline assembly once you set it up (see the sidebar, "Link in Inline Assembly"). This is because VB's compiler uses dynamically named symbols that must match every time you link. You might have to modify and assemble a new assembly source listing every time you make an executable if you modify a native VB listing. On the other hand, a module from a "foreign" language typically contains no dynamically named symbols, which means you can link it in repeatedly with no changes once you compile it.

Detecting and fixing problems in a customized executable requires that you have a debugger. You use a debugger by inserting a DebugBreak API call in your VB (or externally generated) code at the point where you want to start debugging. Use this declaration:

Declare Sub DebugBreak Lib "kernel32" ()

 
Figure 4. Open the Window to VB's Native Code. Click here.

You must also make sure the Generate Symbolic Info checkbox is checked before you compile the project to native code and run the EXE. The Breakpoint Exception message box appears when the program hits your DebugBreak call as long as you have a debugger installed on your system; the Visual C++ installer installs the VC++ integrated debugger automatically (see Figure 4). The debugger shows your VB source code interspersed with the assembly-language instructions and lets you step through the code one instruction at a time.

You should consider making this technique a standard part of your tool chest because you can use it to solve a wide range of VB development problems. For example, you can isolate the source of a GPF by using a debugger to look up its address in the module list. Learning which DLL causes a GPF is often enough to help you pinpoint a problem. You can also use it to debug API calls that result in GPFs. You can only guess what is going wrong when you are working in the VB IDE, but a debugger shows you exactly why the GPF occurs and the contents of the stack—the likely culprit. Note that working with VB's native code temporarily places you on the same footing as the C programmer. This means you must learn to use the C developer's debugging methodologies; VB's interpreter can no longer help you.

Statically Link Non-VB Modules
Statically linking a non-VB object module requires that you first create an interface prototype for it in your project. The prototype is a placeholder that contains no code other than the function definition(s). Compile the individual modules in your project, then substitute the real C (or other language) object module for the placeholder's object module and link the project. For example, assume you want to use a fast, C-authored string routine in your VB app. Begin by creating a stub in a VB basic module called FastString.bas:

Sub FastBStringReverse(ByRef _
   TargetString As String)
   'nothing happens in the stub
End Sub

Next, write the C code you want your app to use:

extern void FastBStrReverse ( 
   unsigned short **BasicString );

void FastBStrReverse ( 
   unsigned short **BasicString )
{
   unsigned long posLeft, posRight;
   unsigned short Swap;

   for (posLeft = 0, posRight = 
      (*BasicString)[-2]/2 - 1; 
      posLeft 
      < posRight; posLeft++, 
      posRight—) {
      Swap = (*BasicString)[posLeft];
      (*BasicString)[posLeft] = 
         (*BasicString)[posRight];
      (*BasicString)[posRight] = Swap;

   }
}

Now step through the compilation, generate a listing for FastString.bas, and stop when VB is ready to link. Examining the project directory at this point reveals a file called FastString.obj. This is the object module VB's compiler (C2.exe) created. Compile the C module with VC++ and generate a listing, taking care that you compile it as a stdcall and turn off any structured exception handling.

Ideally, you'd like to substitute the compiled C module for FastString.obj, but this doesn't work because every compiler uses its own name-decoration scheme. The function name in the object module (called a symbol) generated by VB's second pass compiler (C2) doesn't match the symbol generated by the VC++ compiler:

VB5/6 C2:
?FastBStringReverse@modFastString_
   stub@@AAGXXZ

VC++ CL:
?FastBStrReverse@@YGXPAF@Z

You get around this problem by generating an assembly listing when you compile the C module, substituting the C2 spelling for the CL name, and reassembling (see the sidebar, "Link in Inline Assembly," for details on assembling a listing). Finish the compile by clicking on the Finish Compile button, and VB will link the customized object modules with the rest of the project to create your EXE (or DLL).

The Compile Controller add-in gives you a whole new level of VB programming to explore. For example, changing the arguments to the linker enables you to increase the size of your program's stack, export functions in a DLL, change the entry point of your program, or allow a larger number of modules in your project than is possible in a normal compile. Adding inline assembly enables you to optimize your code, handle API functions with unusual calling conventions and datatypes, call procedures by address, get the address of class methods, and access the registers of the CPU—and that's just for starters. The compiler is now open for experimentation.


John Chamberlain is a long-time VB developer based in Cambridge, Mass., and director of software development at Clinical NetwoRx (). Reach him by e-mail at .

 
Resources
Developing Visual Basic Add-Ins by Steven Roman (O'Reilly & Associates, 1998, ISBN: )

Advanced Windows by Jeffrey Richter (Microsoft Press, 1996, ISBN: )
Get the Code
  Get the code for this article here.