Introduction
A couple of years ago, when I was developing a library of helper classes to further expedite my approach to the way software communicates to its users, I discovered the first of several surprises about the way that debugging with the Visual Studio Hosting Process enabled can cause your program to produce unexpected results. The first of these discoveries arose when I integrated an unmanaged function that reported the execution subsystem of a program, upon which I base decisions about which features of my exception logger class to enable by default. Since I almost always implement such test programs as console applications, I was shocked when the first attempt to use the new routine reported that my test program ran in the Windows GUI subsystem!
Further investigation uncovered the cause, which is that debugging with the Visual Studio hosting process enabled, which it is by default, causes the program that you are debugging to run as a child of the Visual Studio hosting process, a graphical mode program that runs in the Windows GUI subsystem. Further complicating matters, System.Reflection.Assembly.GetEntryAssembly
identifies the hosting process assembly as the entry assembly.
More recently, another, more subtle issue came to my attention, when I noticed that my my console programs displayed incorrect startup times, often minutes, or even hours, before I actually hit F5 to start debugging. Further investigation over the last few days led to the discovery that you have relatively little control over when the Visual Studio hosting process starts, meaning that the System.Diagnostics.Process.StartTime
reports the time when the Visual Studio hosting process started, which may be long before you started debugging.
Beware: Since the default project configuration enables the Visual Studio hosting process, you are using it, unless you disable it. I'll show you how to disable it in the demonstration section, towards the end of the article.
Update: From Visual Studio 2017 onwards, the Visual Studio Hosting Process has been retired. See Debugging and the Hosting Process. Unless you still use Visual Studio 2015 or earlier the sole remaining values of this article are academic and historical. This author won't miss the Visual Studio Hosting Process.
Background
The discussion that follows touches on the following namespaces in the Base Class Library, all of which are exposed by its core libraries.
- System.Reflection
- System.Diagnostics
- System.IO
When I created the helper class that virtually all of my character mode programs use to generate their startup banners and keep track of their wall clock running times, I wanted the startup time to be the time when the process actually started, which might be considerably earlier than when the initial display was formatted and written, especially when you single step the startup. It so happens that the startup and exit times are two of the many properties that Windows keeps in the massive object that springs into being when a new process starts, and stays with it until Windows restarts or assigns the same ID to another process, whichever comes first. Most of these details are accessible to managed code through the System.Diagnostics.Process
object that follows your code for its managed lifetime.
I decided to use the System.Diagnostics.Process.StartTime
as my startup time, and wrote my standard startup routine to save its value into a private System.DateTime
structure, for use both as the startup time in the initial display (the logo or banner), and for eventual use as the starting point for computing the running time of the program, which goes into the last message written on the console. I had not yet discovered that, when the Visual Studio hosting process is enabled, it owns the Process
object, which springs into existence long before your debugging session gets underway, and it usurps the role of Entry Assembly.
What Is the Visual Studio Hosting Process?
Each time you ask the Visual Studio IDE to build a Visual C# or Visual Basic program, it creates two programs.
MyProgram.exe
, where MyProgram
is the name given in the Assembly Name text box on the Application tab of the project property sheet. MyProgram.vshost.exe
is a simple stub program that implements the Visual Studio hosting process for your debugging sessions. For example, Figure 1 shows that the name of the demonstration assembly is VSHostingProcess_Demo
, which becomes VSHostingProcess_Demo.exe
(the demonstration program) and VSHostingProcess_Demo.vshost.exe
(the hosting process assembly), respectively, shown in Figure 2..
Figure 1 is the Application tab of the project property sheet for the demonstration project.
a
Figure 2 is the Debug build output directory, which contains the demonstration assembly, its Visual Studio hosting process assembly, and their configuration files.
The file explorer window shown in Figure 2 shows the files that were created when the project shown in Figure 1 was built.
I noticed early on that the Visual Studio hosting process assembly that went into the output directory of every project appeared to be quite similar. Researching this article prompted me to dig a good bit deeper. Choosing another project that was built at about the same time, and targets the same framework, I started with the side by side directory listings shown in Figure 3, which shows that the two files are exactly the same size.
Figure 3 is a side by side directory listing of the Visual Studio hosting process assemblies from two projects, both targeting Microsoft .NET Framework, Version 3.5 Client Profile.
Both fc.exe
, the byte for byte file comparer that ships with Windows, and IDM Computer Solutions' UltraCompare revealed that, although they are the same size, subtle differences exist in the files.
Comparing them side by side in ILDAsm.exe
yielded the expected result that both assemblies have identical object models and manifests. Figure 4 shows the object models side by side. Digging deeper revealed that all of the generated IL is identical. The proof is left as an exercise for insatiably curious readers, for whom I have provided the necessary resources in VSHostingProcess_SideBySide_20170401_154841.ZIP
.
Figure 4 shows both hosting process assemblies side by side in ILDAsm
windows. The second assembly listed in Figure 3 is on the left. while the demonstration file is on the right.
Satisfied that the two assemblies are substantially identical, I set them aside; perhaps I'll dig deeper, to precisely identify the differences, but that would be a digression from the objective of this article.
A Robust Solution
To address an unrelated issue, I recently began investigating the role of Application Domains, and did a good bit of spelunking in that class and its many members, but that is a subject for another day, and maybe another article.
To understand how I solved the problem, there are two things about Application Domains that you must know.
- Every process has at least one Application Domain, which is exposed by the static
CurrentDomain
property on the System.AppDomain
class. - The default Application Domain of every process that runs under a Visual Studio hosting process has a
DomainManager
property that exposes an EntryAssembly
property, which identifies the real entry assembly (the one you are debugging).
Application domains offer a world of possibilities, and I'll leave it at that. Consider your appetite whetted.
Every Visual Studio hosting process identifies its entry assembly through the CurrentDomain
property of its default AppDomain
. What do you do when the CurrentDomain.DomainManager
property is null? You use the other property, about which I have known for years, and originally used, the Assembly
object returned by the static System.Reflection.Assembly.GetEntryAssembly
method.
- For a hosted process,
Assembly.GetEntryAssembly
returns a reference to the host assembly, e. g., VSHostingProcess_Demo.vshost.exe
. - For a freestanding process,
Assembly.GetEntryAssembly
returns a reference to the expected assembly, the one you are debugging.
I brought up application domains for one very specific reason; they paved the way for the next stage of my research, and for a straightforward solution to the problem, which is enshrined in the private InitializeInstance
method of the PESubsystemInfo
class, which implements the Singleton design pattern, and is shown next, in Listing 1.
if ( _intDefaultAppDomainSubsystemID == IMAGE_SUBSYSTEM_UNKNOWN )
{
Assembly asmDomainEntryAssembly = AppDomain.CurrentDomain.DomainManager != null
? AppDomain.CurrentDomain.DomainManager.EntryAssembly
: Assembly.GetEntryAssembly ( );
_asmDefaultDomainEntryAssemblyName = asmDomainEntryAssembly.GetName ( );
_strDomainEntryAssemblyLocation = asmDomainEntryAssembly.Location;
_intDefaultAppDomainSubsystemID = GetPESubsystemID ( _strDomainEntryAssemblyLocation );
}
Listing 1 is the entire InitializeInstance
method of the PESubsystemInfo
class.
The solution boils down to the first statement inside the IF
block shown in Listing 1, which needs a tad of explanation.
- The current (default) application domain of an application that runs in the Visual Studio hosting process has a
DomainManager
property, and its EntryAssembly
property points to the "real" entry assembly, the one you built, and are testing. - The default domain of an application that starts on its own, without the benefit of a hosting process, has no
DomainManager
, but you can get a reference to its entry assembly by calling the static Assembly.GetEntryAssembly
method.
The remaining statements in the block save the AssemblyName
and Location
properties of the entry assembly into private object variables for immediate use and future reference.
- The
_strDomainEntryAssemblyLocation
property, a simple string, is the fully qualified path (name) of the file from which the entry assembly was loaded, which is put to immediate use to derive its Windows subsystem ID. - The
_asmDefaultDomainEntryAssemblyName
property is an AssemblyName
instance, which exposes the parts of an assembly's full name, which are useful for startup banner strings, window captions, among other things.
Since InitializeInstance
is called when GetTheSingleInstance
is called upon to return a reference to the PESubsystemInfo
singleton, and Reflection calls are relatively expensive, InitializeInstance
short circuits after the first call by taking advantage of the fact that IMAGE_SUBSYSTEM_UNKNOWN
is an invalid value for _intDefaultAppDomainSubsystemID
, and GetPESubsystemID
should reset it.
Identifying the Subsystem in which an Assembly Runs
At last, it's time to turn our attention to the issue that started this expedition, is this a character mode program or a full fledged graphical Windows program?
Every assembly loads from a Windows Portable Executable (PE) file, as does every other program that runs on a Windows operating system. The only exception that is of any significant interest is old 16 bit MS-DOS programs that can still run on 32 bit versions of Microsoft Windows. Strictly speaking, DOS programs don't run under Windows, but use a virtualized MS-DOS machine. Everything else, including command line utilities such as fc.exe
(mentioned above), Xcopy.exe
, RoboCopy.exe
(its successor), cmd.exe
, and its nominal successor, PowerShell.exe
, is a Portable Executable. Henceforth, I shall referee to them as PE files.
The first thousand bytes or so of every PE fie is its PE Header, which has a rather complex format comprised of variable length tables and pointers to their starting locations, augmented by a collection of flags that tell Windows what kind of file it is, how it should be loaded, whether it is a debug build, and lots of other information that is useful to know when a program loads into memory and while it runs. The PE header is fairly well documented in the Windows Platform SDK, and the structures that comprise it are defined in WinNT.h
.
Anyone who has explored the innards of a PE file almost certainly owes Matt Pietrek a debt of gratitude for two articles, "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format," "An In-Depth Look into the Win32 Portable Executable File Format," and "An In-Depth Look into the Win32 Portable Executable File Format, Part 2.," and his famous PEDump.exe
, which is thoroughly described and documented in the original 1994 article (the first of the three cited articles). I certainly do, and most of the code of the original unmanaged (straight C) version of the routine that I wrote to gather this information is adapted from code that comprised a small portion of PEDump.exe
. I also owe my thanks to a former neighbor, Allan Winston, for unearthing the articles, which have moved several times since their initial publication.
After a review of the original C code, I decided that including it here would seriously confuse the story, so I left it out. Anyone who wants to learn how it's done in C can examine PEDump
, which is still available on Matt's Web site.
public static Int16 GetPESubsystemID (
string pstrFileName )
{
const int BEGINNING_OF_BUFFER = 0;
const int INVALID_POINTER = 0;
const int MINIMUM_FILE_LENGTH = 384;
const int NOTHING_READ = 0;
const int PE_HEADER_BUFFER = 1024;
const int PE_HDR_OFFSET_E_LFANEW = 60;
const int PE_HDR_OFFSET_SUBSYSTEM = 92;
const Int16 IMAGE_DOS_SIGNATURE = 23117;
const Int32 IMAGE_NT_SIGNATURE = 17744;
const char QUOTE_CHAR = '"';
if ( string.IsNullOrEmpty ( pstrFileName ) )
throw new ArgumentException (
pstrFileName == null
? Properties.Resources.MSG_GETSUBSYST_NULL_FILENAME_POINTER
: Properties.Resources.MSG_GETSUBSYST_FILENAME_POINTER_EMPTY_STRING );
FileInfo fiCandidate = null;
if ( File.Exists ( pstrFileName ) )
{
fiCandidate = new FileInfo ( pstrFileName );
if ( fiCandidate.Length < MINIMUM_FILE_LENGTH )
{
throw new ArgumentException (
string.Format (
Properties.Resources.MSG_GETSUBSYST_FILE_TOO_SMALL ,
new object [ ]
{
pstrFileName ,
QUOTE_CHAR ,
fiCandidate.Length ,
MINIMUM_FILE_LENGTH ,
Environment.NewLine
} ) );
}
}
else
{
throw new ArgumentException (
string.Format (
Properties.Resources.MSG_GETSUBSYST_FILE_NOT_FOUND ,
pstrFileName ,
QUOTE_CHAR ) );
}
Int16 rintSubystemID = IMAGE_SUBSYSTEM_UNKNOWN;
try
{
int intBytesToRead =
( fiCandidate.Length >= PE_HEADER_BUFFER )
? PE_HEADER_BUFFER
: ( int ) fiCandidate.Length;
int intBytesRead = NOTHING_READ;
byte [ ] abytPeHeaderBuf;
using ( FileStream fsCandidate = new FileStream (
pstrFileName ,
FileMode.Open ,
FileAccess.Read ,
FileShare.Read ) )
{
abytPeHeaderBuf = new byte [ intBytesToRead ];
intBytesRead = fsCandidate.Read ( abytPeHeaderBuf ,
BEGINNING_OF_BUFFER ,
intBytesToRead );
if ( intBytesRead < intBytesToRead )
{
throw new Exception ( string.Format (
Properties.Resources.MSG_GETSUBSYST_FILE_READ_SHORT ,
new object [ ]
{
pstrFileName ,
intBytesRead ,
intBytesToRead ,
Environment.NewLine
} ) );
}
Int16 intPEMagic = BitConverter.ToInt16 ( abytPeHeaderBuf , BEGINNING_OF_BUFFER );
if ( intPEMagic == IMAGE_DOS_SIGNATURE)
{
Int32 intPEOffsetNTHeader = BitConverter.ToInt32 (
abytPeHeaderBuf ,
PE_HDR_OFFSET_E_LFANEW );
if ( intPEOffsetNTHeader > INVALID_POINTER && intPEOffsetNTHeader <= intBytesToRead )
{
Int32 intNTHeaderMagic = BitConverter.ToInt32 (
abytPeHeaderBuf ,
intPEOffsetNTHeader );
if ( intNTHeaderMagic == IMAGE_NT_SIGNATURE )
{
rintSubystemID = BitConverter.ToInt16 (
abytPeHeaderBuf ,
intPEOffsetNTHeader + PE_HDR_OFFSET_SUBSYSTEM );
}
else
{
throw new Exception (
string.Format (
Properties.Resources.MSG_GETSUBSYST_NO_NT_MAGIC ,
pstrFileName ) );
}
}
else
{
throw new Exception (
string.Format (
Properties.Resources.MSG_GETSUBSYST_NO_NT_SIGNATURE ,
pstrFileName ,
intPEOffsetNTHeader ,
Environment.NewLine ) );
}
}
else
{
throw new Exception (
string.Format (
Properties.Resources.MSG_GETSUBSYST_NO_MAGIC ,
pstrFileName ) );
}
}
}
catch ( IOException exIO )
{
throw new Exception (
string.Format (
Properties.Resources.MSG_GETSUBSYST_FILE_READ_ERROR ,
exIO.GetType ( ).FullName ,
pstrFileName ,
Environment.NewLine ) ,
exIO );
}
catch ( Exception exMisc )
{
if ( exMisc.TargetSite.Name == System.Reflection.MethodBase.GetCurrentMethod ( ).Name )
throw;
else
throw new Exception (
string.Format (
Properties.Resources.MSG_GETSUBSYST_GENERAL_EXCEPTION ,
exMisc.GetType ( ).FullName ,
pstrFileName ,
Environment.NewLine ) ,
exMisc );
}
return rintSubystemID;
}
Listing 2 is all of GetPESubsystemID
, which is implemented as a static method on PESubsystemInfo
, and is used internally by its instance initializer.
Extracting information from a PE header is fairly straightforward, especially when you have the PEDump source and the Windows API header files as references. Writing a PE header parser in C# is pretty much the same as it is in C, with two major differences.
- My C# mplementation fills a byte array from a
System.IO.FileStream
object, while the C implementation used a memory-mapped file created from a conventional file handle, which manifests itself as an array of bytes. - Since it didn't have access to the structures defined in
WinNT.h
, my implementation used offsets into the byte array, which were computed manually from the structures defined in WinNT.h
, coupled with calls to static BitConverter
methods to convert the bytes into 16 and 32 bit integers required for testing the magic values that serve as landmarks, and to recover the subsystem ID.
Since this method is exposed to the general public, in a manner of speaking, there are several sanity checks on the filename string that Matt didn't apply to PEDump.
- If the string is a null reference or the empty string, an
ArgumentException
is thrown, using one of two messages that differentiate between a null reference and the empty string. - The string is fed to
System.IO.File.Exists
, which must return TRUE
, or an ArgumentException
is thrown. Note that the exception message includes the string that caused it. One of my pet peeves is exceptions that omit such important details. While there are situations when this is too much information, or disclosing it poses a security risk, I would rather supply the information, and leave that decision in the hands of the caller, than risk it becoming lost because the string is the output of a nested method call. - If the file exists, a
System.IO.FileInfo
object is created around it, and its length is tested. If the file contains fewer than MINIMUM_FILE_LENGTH
(384) bytes, another ArgumentException
arises, which reports the length along with the file name and the threshold that it didn't exceed.
Only after the file has passed all three of the above tests is its first kilobyte read into memory, that being more than enough to ensure that it includes the subsystem ID. Reading the file is the task of a single call to the Read
method on a FileStream
object, which fills a 1024 byte array, and verifies that it got that many.
The rest of the routine is straightforward, and follows the pattern used by my own C code and PEDump
.
Converting the subsystem ID to something meaningful is straightforward. Since there are only 13 values, ranging from zero through 14, with a couple of unassigned values, treating the subsystem ID as an array subscript makes the conversion very straightforward.
- Static array
s_astrShortNames
holds the list of short names described in WinNT.h
. - Static array
s_astrLongNames
holds the list of long names described in WinNT.h
.
To facilitate programmatic testing of subsystem IDs, PESubsystemInfo
defines the PESubsystemID
enumeration, along with a set of Int16
constants for the most common subsystems. For maximum flexibility, I defined both, and made the integer subsystem ID and PESubsystemID
freely convertible in both directions. Internally, the class uses the integer, but everything is accessible through either the raw integer or the enumeration.
Using the Code
The demonstration program is deceptively simple.
- Static member
s_dtmStartedUtc
is a System.DateTime
structure, which is initialized to System.DateTime.UtcNow
by way of a static initializer.
- Using a static member with an initializer guarantees that its value is set as soon as possible.
- Since it must retain its initial value to be usable for computing the total running time of the program, it is marked read only.
- I use UTC time, rather than local time because UTC is unambiguous, since it completely ignores Daylight Saving Time., yet conversion to local time is as easy as calling the
ToLocalTime
instance method.
- Just inside the main routine,
peMainInfo
is defined as a PESubsystemInfo
, and assigned a reference to the single instance of a PESubsystemInfo
. - Next, string
strLogoBanner
is initialized by string.Format
, which is noteworthy for three reasons.
- The same message is written twice, first, to the console, then to the trace log. Otherwise, I would have used the workhorse of character mode output,
Console.WriteLine
. I'll explain about the trace log shortly. - The format items are organized into an array of objects.
- Though I usually use string arrays, and take complete control over the formatting of each item, I chose to accept the default formats, so that I could forego references to the custom helper classes that I use to handle most of the work, or pulling even more of them into the project, which already has adapted versions of two of my helper classes.
- Using a parameter array of objects requires writing less code, because the runtime implicitly calls
ToString
on each object.
- The labeling scheme evident in the line comments is a very deliberate attempt to document the mapping of array elements to format items, and to ensure that there are as many items in the array as there are format items in the format control string..
- The use of trace logging is a bit unorthodox, because the last trace record isn't written until after the operator presses the Return key to exit the program. The objective was to document as accurately as possible when the hosting process exits, because the debugger doesn't time stamp its own entries in the output window, and, since the output is visible for only a split second when you run the code in a debugger (when the timing is most interesting), it goes into the trace log.
- The rest of the demonstration program is unremarkable.
The other three classes deserve a paragraph or so each.
TraceLogger
This static class exports ten methods that cover every imaginable combination of local and UTC time stamps. Only one, WriteWithBothTimesLabeledLocalFirst
, is actually used. I wrote the other nine to complete the set, which I expect to move into a library, in which the entire class will be marked public, and incorporated into the library's namespace.
AppDomainDetails
This static class exports two methods, both used in the main routine. I put them into their own class because I expect them to find their way into a library, quite possibly the same one that gets TraceLogger
. Defining them in a class of their own simplifies incorporating them into another library, because all that is required is to copy the the module, change the namespace, and mark the class public,
GenericSingletonBase
This class is based on an article that I stumbled upon one day, Base Class for Singleton Pattern with Generics.
Update: On Sunday, 01 August 2021, I discovered that the foregoing article has disappeared. Thankfully, it was superseded by a CodeProject article, A Reusable Base Class for the Singleton Pattern in C#.
My implementation resolves the issue raised in the original, since vanished, article regarding the need to provide derived classes with a default constructor, by fitting my abstract base class with a protected default constructor. Since abstract classes must be inherited, and protected methods come along for the ride, derived classes effectively inherit a do-nothing default constructor.,
Periodically, I have debated whether I should add a public GetTheSingleInstance
method, although the static TheOnlyInstance
property essentially fills its role a lot more cheaply. So far, I have concluded that any GetTheSingleInstance
method that I might write into the base class would almost certainly be overridden by the derived class. Note: Though whether it was part of the initial implementation or was added later is lost to history, the current implementation of GenericSingletonBase
in the WizardWrx .NET API class libraries has a public GetTheSingleInstance
method.
Points of Interest
Since I've already covered most of the interesting aspects of the code, I'll use this section to tie up a few loose ends.
Disabling the Visual Studio Hosting Process
Near the beginning of this article, I promised that I would show how to disable the Visual Studio Hosting Process. Like so many things about using Microsoft tools, it's quite simple,, once you know where to look. Display the project Properties by selecting the last item on the Project menu, or by way of its accelerator key, which is ALT-F7, unless you've changed it. Since this page isn't especially busy, it is usually completely visible, unless you have seriously shrunk the main Visual Studio window, and you should see the Visual Studio hosting process setting at the bottom of the page. Figure 5 shows the default values, while Figure 6 shows the hosting process disabled.
As is true with anything else in these property sheets, changing this setting makes the property pages "dirty," and forces a complete project build. Since the setting is stored in the project configuration file, you can also expect your .CPROJ
or .VBPROJ
file to be updated. If the configuration file is under source code control, it will be checked out for editing, using the default locking rule.
Figure 5 shows the default settings on the Debug tab of a project property sheet. Note the check mark labeled "Enable the Visual Studio hosting process."
Figure 6 shows the Debug tab of a project property sheet with the Visual Studio hosting process disabled.
Figure 7 shows the last part of the display as it appears when you run the project with the Visual Studio hosting process enabled. This single picture demonstrates every issue that I mentioned at the top of this article. Not only does this picture demonstrate all of the issues that I raised, but it demonstrates the solution, in the top two rows of text, which reports from the default application domain.
Figure 7 shows the last part of the console output generated by the demonstration program. Note the disparity between the Process Startup Time and the Current Machine Time. This is due to the fact that Visual Studio ran for over an hour before I tapped the F5 key to generate the output shown here.
Figure 8 shows the last part of the console output generated by the demonstration program when it runs with the Visual Studio hosting process disabled. The two time stamps are almost identical, the program name is the name you would expect to see, and the Windows Subsystem ID is 3, Image runs in the Windows character subsystem.
There is a tad more output than will fit on a standard 24-line screen, but not so much that you cannot easily capture all of it by way of the Control menu. Alt-Space bar, E, S, Enter, captures everything,, without leaving the keyboard. The control menu and its edit fly-out menu are shown in Figure 9. Creating this picture required some screen capturing and image editing gymnastics, aided by my stalwart screen capture and image editing tool, JASC Paint Shop Pro 7.02. The trick was to set a timer, and tell it to capture the whole desktop when it expired. The image shown below was created by cropping the desktop image.
Figure 9 shows the context menu and its Edit fly-out menu, which offers the fastest way to capture all of the output from the demonstration program. I used the screen capture timer in Paint Shop Pro, for the first time ever, to allow enough time for me to set up the picture.
Tracing
This project has a TextWriterTraceListener
configured with its output going to an unqualified file, VSHostingProcess_Demo.LOG
, which is created in the build output directory, which is also the working directory used by the program. Somewhat to my surprise, the System.Diagnostics.TextWriterTraceListener
class treats the directory from which the assembly loaded as its current working directory, which could lead to some nasty surprises when you put a new utility program into production.
Zany Stuff
The article template suggests that you say something about anything zany that you did, so here goes.
- When I configured the trace listener, I decided to see whether I could use MSBuild macros to set the output file name. Apparently not, or, at the very least, not without writing a custom build tool to transform the
app.config
file in the source code directory into VSHostingProcess_Demo.exe.config
in the output directory. Oh, well, it was worth a try. - Though not exactly zany, pulling most of the output text from string resources is overkill for a demonstration program. However, since the
PESubsystemInfo
lookup tables and error messages were already coming from resources, I decided that I might as well put everything else in there. - Speaking of
PESubsystemInfo
, the first exception report in GetPESubsystemID
uses a ternary expression to deliver separate exception messages for a null reference and the empty string, making the code that evaluates whether pstrFileName
is either a null reference or the empty string very compact. - I still use my modified Hungarian notation, and I stand by my choice, for two reasons.
- Scope. The first character of the name identifies it as a parameter. I use two other unconventional prefixes, and one common one,
s
, for static, underscore for private, and a
for array. You'll see all three in this sample. - Type. Everything up to the first upper case character succinctly identifies the type, without having to chase down the definition.
That's about it. Happy debugging!
History
Monday, 03 April 2017 - Article published.
Monday, 03 April 2017 - Restore pictures lost by CP sysops.
Monday, 12 June 2017 - Add links to two of the three articles by Matt Pietrek that I cited, giving credit to Allan Winston for finding them, and make a few cosmetic cleanups in the text.
Sunday, 01 August 2021 - Replace the first article cited in the coveage of the GenericSingletonBase class with a citation to the CodeProject article that covers essentially the same material, add an historical note about the demise of the Visual Studio Hosting Process beginning with Visual Studio 2017, and correct a typographical error or two.