Michel's blog
PB 5.0 crash hang dead
Today all of a sudden Platform Builder 5.0 didn't want to load my workspace anymore. Just before I updated the machine using Windows Update so I figured it must have had something to do with that. Platform Builder would start loading the workspace, show "Refreshing the Catalog" in the status bar, then show "Getting variables that correspond with catalog items" and then it would just sit there consuming 50% CPU and not responding to anything else. Luckily I'm using VMWare for all my CE development work so it was easy to revert back to an earlier snapshot before I updated XP. Unfortunately, this didn't solve the problem! I continued my search and after some frustrating hours I found the cause:
A bit earlier in the day I added a whole lot of header files (121 to be precise) to one of my subprojects sources file (using the FILE_VIEW_INCLUDES_FOLDER macro), and this proved to be the problem. After I reverted the sources file back to its previous revision in SVN the project loaded again.
Another one of those Platform Builder 5.0 mysteries solved...
Manual Clone of Public Code
If sysgen_capture doesn't work for some reason you can always manually clone the component you want to modify. By following the instructions on the blog post "Cloning Public Code: An Example" you should be able to get this working, but to help you a bit more here are some step by step instructions for manually cloning public code.
In this blogpost we'll be cloning TELNETD because for some reason sysgen_capture doesn't work for this component. TELNETD is located under the WINCE project tree "servers". I know the exact name of the WINCEPROJ by opening up the makefile in \PUBLIC\SERVERS\CESYSGEN and searching for WINCEPROJ. The correct sysgen_capture command for TELNETD would be:
sysgen_capture -p servers telnetd
The -p parameter indicates the WINCEPROJ, so in this case it's of course not "common", but "servers". Unfortunately for some reason this command doesn't output a sources.telnetd file (in fact it doesn't output any file!). No stress, let's just clone manually:
- Copy the TELNETD folder to your OS Design (
\WINCE500\PBWorkspaces\<MyOSDesign>\TELNETDor\WINCE600\OSDesigns\<MyOSDesign>\<MyOSDesign>\TELNETD) - Open the
sources.file in the folder you just copied - Set
TARGETTYPEtoDYNLINK - Set
RELEASETYPEtoLOCAL - Delete
WINCETARGETFILE0=$(_RELEASELIBDIR)\$(TARGETDEFNAME).def - Open the
makefile.in<WINCEROOT>\PUBLIC\SERVERS\CESYSGENand search fortelnetd - Copy the line
@set TARGETLIBS...and@set DLLENTRY... - Paste into your
sources.file - Change
@set TARGETLIBS...in yoursources.file to:
TARGETLIBS=$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\ws2.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\coredll.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\ceosutil.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\authhlp.lib - Change
@set DLLENTRY...in yoursources.file to:
DLLENTRY=DllEntry - Add the include paths:
NOTE: DO NOT PUT SPACES BETWEEN THE PATHS! I know it is confusing because I do it above, but I have to to support line breaking (otherwise this page will look ugly...)._OEMINCPATH=$(_WINCEROOT)\public\common\oak\inc; $(_WINCEROOT)\public\common\sdk\inc; $(_WINCEROOT)\public\common\ddk\inc - And build...
Easy as that! Now that you have the TELNETD component successfully cloned you can continue following the instructions from step 7 in the blog post "Cloning Public Code: An Example" (of course replacing "netui" with "telnetd" in the text).
Here's the complete sources. file for reference (don't forget to delete the spaces in the _ISVINCPATH, _OEMINCPATH and INCLUDES macros! I need those spaces here to format the HTML nicely, but you will get compiler error "/I" requires an argument if you leave those spaces):
_OEMINCPATH=$(_WINCEROOT)\public\common\oak\inc; $(_WINCEROOT)\public\common\sdk\inc; $(_WINCEROOT)\public\common\ddk\inc
TARGETNAME=telnetd
TARGETTYPE=DYNLINK
RELEASETYPE=LOCAL
TARGETDEFNAME=$(TARGETNAME)
DEFFILE=$(TARGETNAME).def
CDEFINES=$(CDEFINES) -DwinCE
DLLENTRY=DllEntry
SOURCES=telnetd.cpp telndev.cpp consemu.cpp
TARGETLIBS=$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\ws2.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\coredll.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\ceosutil.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\authhlp.lib
Cloning public code: An example
For some strange reason, people are still changing public code and doing build and sysgen's on their Windows CE tree (read this to learn why this is a bad thing). Changing code in the PUBLIC and PRIVATE trees might seem like a shortcut but actually it is everything but a shortcut. When you clone the code before changing it in place you will save yourself hours and hours of build time. A targeted build of a cloned driver takes seconds, a clean build and sysgen takes hours.
Cloning is not difficult at all, so there is really no reason to change PUBLIC or PRIVATE code. To prove that it is not difficult (once you know what you are doing) I will show you how to clone PUBLIC code in this blog post. Note that there is an article by Steve Maillet and Mike Hall in MSDN about cloning public code too. They split the clone into a "LIB" part and a "DLL" part. I don't really like that method; I just merge the sources file into one that creates the DLL in one go. Have a look at the "split" method here.
I chose to clone NETUI, since this component is responsible for a lot of dialog boxes that you may want to change to match your systems specifications.
I am cloning NETUI on a Windows CE 6.0 tree, but it works exactly the same on a Windows CE 5.0 tree (apart from the OSDesign folder which is called PBWorkspaces in CE 5.0).
Prerequisites: A sysgenned OSDesign with the "Network UI" component included.
Let's start!
- Copy the entire
\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\NETUIfolder to your OSDesign, for instance\WINCE600\OSDesigns\MyOS\MyOS - Open a build window (in Platform Builder / VS2005 click "Open Release Directory in Build Window" from the menu "Build"
- In the build window, type:
cd..
cd..
cd netui - Now type:
This command will output a set of files in the current directory.sysgen_capture -p common netui - Since we only want to build netui, you can delete:
sources.btdrt
sources.ceddk
sources.iphlpapi
sources.k.iphlpapi
sources.ws2
sources.ws2k - Now intelligently merge
sources.withsources.netui(we make the changes in thesources.file): - Make a backup of the
sources.file (copy tosources.org) so you can always go back if needed - We are trying to build a DLL, so change
TARGETTYPE=LIBRARYtoTARGETTYPE=DYNLINK - We are building NETUI as a project in our workspace, so set the
RELEASETYPEtoLOCAL(RELEASETYPE=LOCAL) - We are not building a LIB anymore, so there's no need to preprocess the def file. Remove the following lines:
WINCETARGETFILE0=$(_RELEASELIBDIR)\$(TARGETDEFNAME).def
PREPROCESSDEFFILE=1 - We don't need the resource file later (because we are not creating a lib that has to be transformed into a dll later), so remove the
COPYRES=1line and theWINCETARGETFILESline pointing to the res. - OSDesign SubProjects get build last, so we don't need to copy
SYNCHRONIZE_DRAIN=1fromsources.netuito oursources.file - Since we are now building a DLL, copy the
DLLENTRYline fromsources.netuitosources. - Now move the entire
SOURCELIBSandTARGETLIBSfromsources.netuitosources. - and delete any references to netui (
netui.lib/netui.res) itself (because we are building that now) - Change the paths to the target libraries from using the PUBLIC sysgened libraries to the libraries sysgened to your workspace, so change
$(_SYSGENSDKROOT)\to$(_PROJECTROOT)\cesysgen\sdk\and$(_SYSGENOAKROOT)\to$(_PROJECTROOT)\cesysgen\oak\ - You can also delete the
#xreflines, they are remains of older days (CE 2.11 era). - Now try to build (by typing "
build" in the build window you still have open) - oops! Doesn't build. It can't find the standard include files... Add the following to the top of the sources file:
NOTE: DO NOT PUT SPACES BETWEEN THE PATHS! I know it is confusing because I do it above, but I have to to support line breaking (otherwise this page will look ugly...)._ISVINCPATH=$(_WINCEROOT)\public\common\sdk\inc;
_OEMINCPATH=$(_WINCEROOT)\public\common\ddk\inc; $(_WINCEROOT)\public\common\oak\inc; $(_WINCEROOT)\public\common\sdk\inc; - And since this project is building OEM code (a kernel component) set
WINCEOEM=1to indicate we want to use the _OEMINCPATH (so if you really want to you can delete the_ISVINCPATHsince we don't use that one). - Try to build again:
BUILD: [01:0000000048:ERRORE] \WINCE600\OSDesigns\MyOS\MyOS\NETUI\.\btmgmtui.cpp(39) : fatal error C1083: Cannot open include file: '../bluetooth/sample/btenum/btenum.hxx': No such file or directory
So, one of the sources files is trying to include a header on a relative path. Since NETUI came from\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\NETUIthis path must be\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\BLUETOOTHNow there are 3 ways to solving this: Either add the
\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\NETUIfolder to the include path (addINCLUDES=$(INCLUDES);\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\NETUI) so that the header file can be found off the relative path, or change the source file including the header to use a full path to the header file, or copy thebtenum.hxxfile into the NETUI folder and change the source file including the header to use the local header file. I think the last option is the cleanest, so let's copy\WINCE600\PUBLIC\COMMON\OAK\DRIVERS\BLUETOOTH\SAMPLE\BTENUM\btenum.hxxto\WINCE600\OSDesigns\MyOS\MyOS\NETUI\btenum.hxxand changebtmgmtui.cppto#include "btenum.hxx" - Build again:
0 Warnings, 0 Errors.Whuuhooo! - Now there is one final thing we have to add to the NETUI
sources.file. We have to tell the build system we want to copy the final binary to the_FLATRELEASEDIRso ournetui.dllwill overwrite the default one from thePUBLIC\COMMON folder. AddWINCEREL=1to the top of thesources.file. - Now add the NETUI project to your workspace. In the Platform Builder VS IDE click "Add Existing Subproject..." from the "Project" menu. Browse to the
\WINCE600\OSDesigns\MyOS\MyOS\NETUIfolder, select "Sources/Dirs Files" from the "Files of type" dropdown list, select thesources.file, and click "Open" - In the Solution Explorer window you can now find your NETUI component under the "SubProjects" node. Right click NETUI and click "Rebuild" to see if you can build the project from the IDE. It should all build fine.
- If all builds fine, you can delete the
sources.netuiand thesources.orgfiles since you no longer need them now. - The "Insert existing project" action created a couple of files for you. One of those files is a bib file. BIB files are used to indicate which files need to be included in the image. Since we cloned NETUI, but did not remove it from the OSDesign features, we now have 2 references to NETUI in the combined BIB files (one is in
COMMON.BIBand the other in our newly generatedNETUI.BIB). We don't need an extra reference in the BIB files, all we want is overwrite the defaultNETUI.DLLin the_FLATRELEASEDIR, so you can remove this reference from the newly generatedNETUI.bib.
And add this line to tell the build system to use the local def file for the exports:
Now you can change whatever you need to change in NETUI without worrying about messing up the PUBLIC portion of the WINCE tree or having to do a "Clean Build and Sysgen".
If you have followed all the steps above your final NETUI sources. file should look like this (except the spaces in the _ISVINCPATH, _OEMINCPATH and INCLUDES macros!):
_OEMINCPATH=$(_WINCEROOT)\public\common\ddk\inc; $(_WINCEROOT)\public\common\oak\inc; $(_WINCEROOT)\public\common\sdk\inc;
WINCEOEM=1
WINCEREL=1
TARGETNAME=netui
TARGETTYPE=DYNLINK
RELEASETYPE=LOCAL
TARGETDEFNAME=$(TARGETNAME)
DEFFILE=$(TARGETDEFNAME).def
DLLENTRY=_DllEntryCRTStartup
CONDITIONAL_INCLUDES=prshtp.h
SOURCELIBS=
TARGETLIBS=\
$(_PUBLICROOT)\common\oak\lib\$(_CPUINDPATH)\btenum.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\iphlpapi.lib \
$(_PROJECTROOT)\cesysgen\oak\lib\$(_CPUINDPATH)\btdrt.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\ws2.lib \
$(_PROJECTROOT)\cesysgen\oak\lib\$(_CPUINDPATH)\ceddk.lib \
$(_PROJECTROOT)\cesysgen\sdk\lib\$(_CPUINDPATH)\coredll.lib
SOURCES= \
netui.c \
getip.c \
ipaddr.c \
getuser.c \
linecnfg.c \
wnet.c \
netcard.c \
certui.cpp \
showcert.cpp \
eaptlscfg.cpp \
network.c \
transdlg.c \
reg.c \
util.c \
wzcui.c \
wzcprops.c \
btmgmtui.cpp \
WzcQuickCfgUi.c \
IpQuickCfgUi.c \
QuickConfigUi.c \
WzcLogging.c \
WzcPopup.c \
netui.rc \
wnet_wrapper.cpp \
netui_wrapper.cpp \
getuser_wrapper.cpp \
gwes_wrapper.cpp \
getip_wrapper.cpp \
linecnfg_wrapper.cpp \
transdlg_wrapper.cpp \
network_wrapper.cpp \
netcard_wrapper.cpp
FILE_VIEW_PARAMETER_FOLDER= \
NETUI.bib \
NETUI.reg \
NETUI.dat \
NETUI.db \
ProjSysgen.bat \
FILE_VIEW_ROOT_FOLDER= \
prelink.bat \
postlink.bat \
PRELINK_PASS_CMD=prelink.bat
POSTLINK_PASS_CMD=postlink.bat
Filtering dat files
If you read this blog or the CE newsgroups you should by now know you should never change any files in the PUBLIC or PRIVATE folders of your WINCE installation tree. So what if you want to change a setting in common.reg (which is located in the PUBLIC folder)? Well, add the same entry to your platform.reg (in your PLATFORM\BSP folder) and set it to whatever you like. So, what if you want to delete a setting from common.reg? Again, add the same entry to your platform.reg but precede the key name with a "-". This works because platform.reg is merged into reginit.ini after common.reg, so any duplicate entries or deletion commands (the "-") will override the settings in common.reg.
So, what about dat files? Unfortunately, deleting dat entries with the "-" tag is not supported. Dat files are responsible for the initial copy of files and creation of folders during boot. For instance, dat files are used to copy shortcuts from the \Windows folder to the desktop of your CE device. Now what if you don't want to have icons on your desktop? The only way that seems possible is to actually change the public dat files in place, but you know you should never change any files in the PUBLIC tree! We just have to come up with a different solution...
The solution in this case is to filter the dat files and remove any unwanted lines before creating the final image but after the sysgen phase and without touching the original files in PUBLIC. Windows CE calls out to a number of batch files during the makeimg process to let you write your own scripts to affect the image at different points during the image building process. One of those batch files is "PostFmergeObj.bat" which, as the name suggests, is called just after fmerge merged the "Obj" files (which are in fact "Dat" files... confused already? ;o) into your FLATRELEASEDIR.
By using a bit of scripting magic we can use this batch file to filter out unwanted lines from the final merged InitObj.dat file:
- REM This batch file filters the DAT files
- REM CE merges all DAT files into initobj.tmp
- REM and then transforms initobj.tmp into a
- REM UNICODE version initobj.dat.
- REM Just before the conversion to unicode
- REM the build system calls PostFmergeObj.bat
- REM (this file). We filter out all the
- REM strings defined in PostFmergeObj.txt
- REM from initobj.tmp
- @echo off
- echo PostFmergeObj.bat entry.
- pushd %_FLATRELEASEDIR%
- echo Deleting root:- entries from initobj.tmp
- del initobj.org
- ren initobj.tmp initobj.org
- findstr /i /v /g:PostFmergeObj.txt initobj.org > initobj.tmp
- popd
- echo PostFmergeObj.bat exit.
- @echo on
The core of the script is from line 15 onwards. In line 15 we delete any previous initobj.org file this batch file may have left behind. Then, in line 16, we rename initobj.tmp (all dat files are merged into this file by fmerge) to initobj.org and in line 17 we call the dos utility Findstr:
Searches for strings in files.
FINDSTR [/B] [/E] [/L] [/R] [/S] [/I] [/X] [/V] [/N] [/M] [/O] [/P] [/F:file]
[/C:string] [/G:file] [/D:dir list] [/A:color attributes] [/OFF[LINE]]
strings [[drive:][path]filename[ ...]]
/B Matches pattern if at the beginning of a line.
/E Matches pattern if at the end of a line.
/L Uses search strings literally.
/R Uses search strings as regular expressions.
/S Searches for matching files in the current directory and all
subdirectories.
/I Specifies that the search is not to be case-sensitive.
/X Prints lines that match exactly.
/V Prints only lines that do not contain a match.
/N Prints the line number before each line that matches.
/M Prints only the filename if a file contains a match.
/O Prints character offset before each matching line.
/P Skip files with non-printable characters.
/OFF[LINE] Do not skip files with offline attribute set.
/A:attr Specifies color attribute with two hex digits. See "color /?"
/F:file Reads file list from the specified file(/ stands for console).
/C:string Uses specified string as a literal search string.
/G:file Gets search strings from the specified file(/ stands for console).
/D:dir Search a semicolon delimited list of directories
strings Text to be searched for.
[drive:][path]filename
Specifies a file or files to search.
Use spaces to separate multiple search strings unless the argument is prefixed
with /C. For example, 'FINDSTR "hello there" x.y' searches for "hello" or
"there" in file x.y. 'FINDSTR /C:"hello there" x.y' searches for
"hello there" in file x.y.
Regular expression quick reference:
. Wildcard: any character
* Repeat: zero or more occurances of previous character or class
^ Line position: beginning of line
$ Line position: end of line
[class] Character class: any one character in set
[^class] Inverse class: any one character not in set
[x-y] Range: any characters within the specified range
\x Escape: literal use of metacharacter x
\<xyz Word position: beginning of word
xyz\> Word position: end of word
For full information on FINDSTR regular expressions refer to the online Command
Reference.
By calling findstr with options /I and /V we effectively filter out the lines we specify in PostFmergeObj.txt and we pipe the output of findstr into InitObj.tmp. After this batch file returns the build system will call the "Text to Unicode" tool (Txt2ucde.exe) that converts any ASCII text strings in Initobj.tmp to Unicode text strings and creates the file Initobj.dat which then contains the Unicode conversion of InitObj.tmp (see http://msdn.microsoft.com/en-us/library/ms930234.aspx).
Note that you can also use regular expressions to find strings with findstr. Using this you have a very powerful tool to filter whatever file you want filtered and of course this trick doesn't only work on dat files, it works on any ascii text file (like bib, reg, etc).
All you have to do for all this to start working in your build is create PostFmergeObj.bat in your PLATFORM\BSP\FILES folder and copy'n'paste the above script. The Windows CE build system will automatically search for this batch file and if it finds it execute it just after it has merged the various configuration files. Don't forget to create a "PostFmergeObj.txt" file with the lines you want to delete from the final InitObj.dat file in the same folder as the batch file, like this:
Directory("\Windows\www"):-Directory("wwwpub")
root:-Directory("Program Files")
root:-Directory("My Documents")
root:-File("Control Panel.lnk", "\Windows\control.lnk")
Directory("\Windows"):-Directory("Desktop")
Directory("\Windows"):-Directory("Programs")
Directory("\Windows"):-Directory("Recent")
Directory("\Program Files"):-File("Command Prompt.lnk","\Windows\cmd.lnk")
Directory("\Windows\Programs"):-File("Windows Explorer.lnk", "\Windows\explore.lnk")
Directory("\Windows\Programs"):-File("Command Prompt.lnk","\Windows\cmd.lnk")
Directory("\Program Files"):-File("Command Prompt.lnk","\Windows\cmd.lnk")
When you now perform a "make image" this batch file should automatically execute and its output (the "echo" commands in the batch file) should appear in the build output window.
.NET Micro Framework v3.0 SDK Beta
The first public beta of version 3.0 of the .NET Micro Framework SDK just got released through the Microsoft Connect website, check out the new features!
A template for debug messages
In all the drivers and applications I develop, I always take the time to add plenty of logging because I know good logging will save me loads of time debugging my code (as if I ever have to "debug" my perfect code, ha! ;o).
Windows CE has a really great mechanism for debug logging called Debug Zones. This mechanism allows you to divide your debug messages into 16 zones and each of these zones can be turned on or off at run-time. This allows you to control the verbosity of your debug output and helps prevent cluttering of messages. Read this excellent blog post by Travis first before reading on: http://blogs.msdn.com/ce_base/archive/2006/12/18/debug-messages-and-debu...
To add debug logging using Debug Zones to your application or driver do the following:
1. Define your zones using the DEBUGZONE macro
1. Define a DBGPARAM structure and fill it
2. Call either DEBUGREGISTER or RETAILREGISTERZONES from your DllMain (driver) or WinMain (application)
3. Log messages using either DEBUGMSG or RETAILMSG
Sounds easy (and actually it is) but to make it more easy (and because I found myself doing the same thing over and over) I created a template I now use in all my driver and application code, even on projects targeted for Desktop Windows. Even though "Big" Windows doesn't have a "Debug Zone" mechanism you can still use the template. The big benefit of course being your code is completely portable between desktop PC's and CE devices (and also MBS/UNICODE)! The only thing you can't do is switch debug zones at run-time, but you can still select which zones should be on or off at compile-time.
Let me explain the template by walking through it; I'll start with DebugZones.cpp (the line numbers reflect the actual line numbers in the template files):
- #include <windows.h>
- #include "DebugZones.h"
- // Do NOT change anything in this file.
- // See DebugZones.h for things you can change!
- /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
- DBGPARAM dpCurSettings =
- {
- __TMODULE__,
- {
- ZONE0_TEXT,ZONE1_TEXT,ZONE2_TEXT,ZONE3_TEXT,
- ZONE4_TEXT,ZONE5_TEXT,ZONE6_TEXT,ZONE7_TEXT,
- ZONE8_TEXT,ZONE9_TEXT,ZONE10_TEXT,ZONE11_TEXT,
- ZONE12_TEXT,ZONE13_TEXT,ZONE14_TEXT,ZONE15_TEXT
- },
- #ifndef DEBUG // IN RETAIL...
- RETAILZONES
- #else //IN DEBUG...
- DEBUGZONES
- #endif
- };
The first and most important thing to note is that you should not change anything in this file. You configure the template all through the header file, which I will be discussing later.
In this first part of DebugZones.cpp I simply define a global variable dpCurSettings of type DBGPARAM and fill the structure with defines from DebugZones.h. The __TMODULE__ corresponds to the "lpszName" member, the ZONEx_TEXT defines fill the "rglpszZones" array and finally RETAILZONES or DEBUGZONES (depending on whether you build a "debug" or "release" version of the code) corresponds to the "ulZoneMask" member of the DBGPARAM structure. The CE debug system uses this structure to get Debug Zone information from the module and to set or clear debug zones during execution of the module. When you use the template for code targeted at Desktop Windows this structure will only be used by the DEBUGMSG/RETAILMSG macros (they use the ulZoneMask member as a display condition).
- #ifndef UNDER_CE
- #include <stdio.h>
- void WINAPIV NKDbgPrintf(LPCTSTR lpszFmt, ...)
- {
- #define BUFFER_SIZE 1000
- va_list argptr;
- va_start(argptr, lpszFmt);
- TCHAR szBuffer[BUFFER_SIZE];
- _vstprintf_s(szBuffer, BUFFER_SIZE, lpszFmt, argptr);
- OutputDebugString(szBuffer);
- va_end(argptr);
- }
- #endif
This code is only used when you target Desktop Windows. Only Windows CE defines NKDbgPrintf so for Desktop Windows I had to create my own implementation of this function to make the Debug Zones system portable. This function simply takes a format string followed by a variable number of parameters and transforms that into a formatted string and outputs it to the debug output window of Visual Studio. Note that the maximum length of the formatted string is BUFFER_SIZE characters.
- // Helper function for runtime conversion of char* to _T
- LPCTSTR to_T(LPCSTR pStr)
- {
- #define MAX_CALLS 4
- #define MAX_MSG 500
- static TCHAR buffer[MAX_CALLS][MAX_MSG];
- static DWORD index = 0;
- LPCTSTR pszRet = buffer[index];
- size_t conv;
- #ifdef UNICODE
- mbstowcs_s(&conv, buffer[index], MAX_MSG, pStr, MAX_MSG);
- #else //ANSI
- strcpy_s(buffer[index], MAX_MSG, pStr);
- #endif
- index++;
- if (MAX_CALLS==index)
- index = 0;
- return pszRet;
- }
This next bit of code is a helper function that solves a problem when you want your code to be portable between MBS and UNICODE. MBS is multi-byte string, also called ANSI string, and UNICODE is, well, UNICODE strings. Let's say you are compiling for UNICODE but want to display a multi-byte string using a function that uses a format string. The way to do that is to use %S instead of %s in the format string. When you use %s in code compiled for UNICODE the variable indicated by %s should be in UNICODE. When you compile for MBS the variable indicated by %s should be MBS. In other words, %s indicates the "native" string format. If you want to display a multi-byte string in code compiled for UNICODE you'd use %S, and if you want to display a UNICODE string in code compiled for MBS you'd use %S as well. In other words, %S indicates the "opposite" string format. And here lies the problem: What if you want to display a multi-byte string regardless of whether the code is compiled for MBS or UNICODE?
Take a look at the following code:
NKDbgPrintf(_T("Display opposite string format \"%S\"\r\n"), "This is a multi-byte string");
Note that the _T macro is defined as L in UNICODE (making the following string a UNICODE string) and as nothing in MBS (leaving the following string a multi-byte string).
When compiled for UNICODE the above code works perfectly:
Display opposite string format "This is a multi-byte string"
But now see what happens when you compile the above code for MBS:
You only get the output of the first line! So what just happened? Well, the _vstprintf_s function in NKDbgPrintf expected the first argument to be of the opposite string format. Since you compiled for MBS this means it expects a UNICODE string. The _vstprintf_s function detects it is not and bails out, setting szBuffer to an empty string.
The to_T function solves this problem:
NKDbgPrintf(_T("Display opposite string format \"%s\"\r\n"), to_T("This is a multi-byte string"));
When compiled for MBS the output is:
Display opposite string format "This is a multi-byte string"
I can here you say: So why don't you just use _T() in that 2nd call to NkDbgPrintf? And yes, you are right, in the above code that would work, but what if it's a variable pointing to a multi-byte string? You can't use _T() around a variable:
Line 64 defines the maximum number of times you can call this function before it will start overwriting its internal buffers. Increase this value if you want to use more than 4 parameters that need to be converted in one call. To understand this, see the following code and its output:
char* s2 = "String 2";
char* s3 = "String 3";
char* s4 = "String 4";
char* s5 = "String 5";
char* s6 = "String 6";
NKDbgPrintf(_T("%s %s %s %s %s %s\r\n"), to_T(s1), to_T(s2), to_T(s3), to_T(s4), to_T(s5), to_T(s6));
Output:
Let's move on to the real core of the template; DebugZones.h:
- #pragma once
- // Change the following definitions:
- /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
- #define __MODULE__ "MyDriver" // The module name
- // Definitions for our debug zones
- #define ZONE0_TEXT _T("Init")
- #define ZONE1_TEXT _T("Function")
- #define ZONE2_TEXT _T("Memory")
- #define ZONE3_TEXT _T("")
- #define ZONE4_TEXT _T("")
- #define ZONE5_TEXT _T("")
- #define ZONE6_TEXT _T("")
- #define ZONE7_TEXT _T("")
- #define ZONE8_TEXT _T("")
- #define ZONE9_TEXT _T("")
- #define ZONE10_TEXT _T("")
- #define ZONE11_TEXT _T("")
- #define ZONE12_TEXT _T("")
- #define ZONE13_TEXT _T("Info")
- #define ZONE14_TEXT _T("Warning")
- #define ZONE15_TEXT _T("Error")
- // These macros can be used as condition flags for LOGMSG
- #define ZONE_INIT DEBUGZONE(0)
- #define ZONE_FUNCTION DEBUGZONE(1)
- #define ZONE_MEMORY DEBUGZONE(2)
- #define ZONE_3 DEBUGZONE(3)
- #define ZONE_4 DEBUGZONE(4)
- #define ZONE_5 DEBUGZONE(5)
- #define ZONE_6 DEBUGZONE(6)
- #define ZONE_7 DEBUGZONE(7)
- #define ZONE_8 DEBUGZONE(8)
- #define ZONE_9 DEBUGZONE(9)
- #define ZONE_10 DEBUGZONE(10)
- #define ZONE_11 DEBUGZONE(11)
- #define ZONE_12 DEBUGZONE(12)
- #define ZONE_INFO DEBUGZONE(13)
- #define ZONE_WARNING DEBUGZONE(14)
- #define ZONE_ERROR DEBUGZONE(15)
- // These macros can be used to indicate which zones to enable by default (see RETAILZONES and DEBUGZONES below)
- #define ZONEMASK_INIT ZONEMASK(0)
- #define ZONEMASK_FUNCTION ZONEMASK(1)
- #define ZONEMASK_MEMORY ZONEMASK(2)
- #define ZONEMASK_3 ZONEMASK(3)
- #define ZONEMASK_4 ZONEMASK(4)
- #define ZONEMASK_5 ZONEMASK(5)
- #define ZONEMASK_6 ZONEMASK(6)
- #define ZONEMASK_7 ZONEMASK(7)
- #define ZONEMASK_8 ZONEMASK(8)
- #define ZONEMASK_9 ZONEMASK(9)
- #define ZONEMASK_10 ZONEMASK(10)
- #define ZONEMASK_11 ZONEMASK(11)
- #define ZONEMASK_12 ZONEMASK(12)
- #define ZONEMASK_INFO ZONEMASK(13)
- #define ZONEMASK_WARNING ZONEMASK(14)
- #define ZONEMASK_ERROR ZONEMASK(15)
- #define RETAILZONES ZONEMASK_INIT | ZONEMASK_WARNING | ZONEMASK_ERROR
- #define DEBUGZONES RETAILZONES | ZONEMASK_FUNCTION | ZONEMASK_MEMORY | ZONEMASK_INFO
- // Define FULL_CONTEXT to show full context in debug messages:
- // "Module: [full path to file name:line number] Function>"
- //#define FULL_CONTEXT
- /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
This is the part of the template you should change to match your driver or application. First, in line 30, change the __MODULE__ define to the name of your driver or application. If you are creating a flash driver you'd set this to "FLASH" for instance.
Next, in lines 33 to 48, you define the friendly names of the debug zones you want. These names will show up when you use the Debug Zones dialog or the "zo" command from Platform Builder. The predefined ones are always useful:
- Init
- Function
- Memory
- Info
- Warning
- Error
Use this zone to display debug messages during the initialization phase of your driver. I use this zone in the Init function of drivers.
This zone is used to display function entry and exit. Used by the FUNC_ENTRY and FUNC_EXIT macros (I'll discuss those later)
This zone is used to display memory allocation and de-allocation. Having a separate zone for memory really helps in identifying memory leaks or memory related problems in your driver or application (besides CeDebugX)
This zone is used to display general information. I use this zone a lot through-out my code and enabling this zone usually means I get heaps of debug messages, but also heaps of information about the flow of my code
Use this zone for non-fatal errors (also known as warnings ;o)
Use this zone for fatal errors; situations where you had to abort a certain operation
Another zone I use often is "IOCTL". If my driver handles custom IOCTLs I use that zone to display information when those IOCTLs are handled. If you want to add the IOCTL zone you'd change line 36 to:
- #define ZONE3_TEXT _T("IOCTL")
Lines 51 to 66 define the actual zone IDs. You use these defines as the first parameter of LOG_MSG (that I'll discuss later) to indicate the zone this message belongs to. After you added the IOCTL zone to the friendly names list, you'd change line 54 to:
- #define ZONE_IOCTL DEBUGZONE(3)
Lines 69 to 84 define the zone masks. You use these defines to indicate which zones should be enabled by default. After you added the IOCTL zone to the zone ID list, you'd change line 72 to:
- #define ZONEMASK_IOCTL ZONEMASK(3)
Line 86 and 87 define the default zones to use in a release build and a debug build. I enable zones INIT, WARNING and ERROR in a release build and add FUNCTION, MEMORY and INFO to debug builds. Note that in a "ship" build (on Windows CE, WINCESHIP=1) debug/retail messages are defined as "nothing" (so all the logging code is effectively removed from the final binary).
If you define FULL_CONTEXT (line 91) the LOGMSG macro will output the full path to the source code filename in front of the message, instead of just the module name. I don't enable this by default because it makes the debug output cluttered and less readable (but it can be very handy in some cases).
This concludes the part of DebugZones.h that you should change. The remainder of the code mostly deals with the differences between Desktop Windows and Windows CE:
- // Do NOT change any of the following definitions:
- /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
- LPCTSTR to_T(LPCSTR pStr);
- #ifdef UNDER_CE
- #ifndef DEBUG
- #include <windows.h>
- extern DBGPARAM dpCurSettings;
- #endif
- #else
- #include <windows.h>
- #include <tchar.h>
- #include <stdio.h>
- typedef struct _DBGPARAM {
- TCHAR lpszName[32]; // @field Name of module
- TCHAR rglpszZones[16][32]; // @field names of zones for first 16 bits
- ULONG ulZoneMask; // @field Current zone Mask
- } DBGPARAM, *LPDBGPARAM;
- #ifndef DEBUG
- #ifdef _DEBUG
- #define DEBUG
- #endif
- #endif
- extern DBGPARAM dpCurSettings;
- void WINAPIV NKDbgPrintf(LPCTSTR lpszFmt, ...);
- #define RETAILMSG(cond,printf_exp) \
- ((cond)?(NKDbgPrintf printf_exp),1:0)
- #define DEBUGZONE(n) (dpCurSettings.ulZoneMask & (0x00000001<<(n)))
- #define RETAILREGISTERZONES(hMod)
- #ifdef DEBUG
- #define DEBUGMSG(cond,printf_exp) \
- ((void)((cond)?(NKDbgPrintf printf_exp),1:0))
- #define DBGCHK(module,exp) \
- ((void)((exp)?1:( \
- NKDbgPrintf ( _T("%s: DEBUGCHK failed in file %s at line %d \r\n"), \
- module, _T(__FILE__) ,__LINE__ ), \
- DebugBreak(), \
- 0 \
- )))
- #define DEBUGCHK(exp) DBGCHK(dpCurSettings.lpszName, exp)
- #define DEBUGREGISTER(hMod)
- #endif //DEBUG
- #endif
- #ifndef __FUNCTION__
- #define __FUNCTION__ __FILE__
- #endif
- #ifdef UNICODE
- #define PREPT(x) L ## x
- #else
- #define PREPT(x) x
- #endif
- #define TOTSTR(x) PREPT(x)
- #define __TFUNCTION__ TOTSTR(__FUNCTION__)
- #define __TMODULE__ TOTSTR(__MODULE__)
- #define __TFILE__ TOTSTR(__FILE__)
- #define ZONEMASK(n) (0x00000001<<(n))
- #ifdef FULL_CONTEXT
- #define FUNC_ENTRY(fmt, ...) RETAILMSG(ZONE_FUNCTION, (__TMODULE__ _T(": +[") __TFILE__ _T(":%d] ") __TFUNCTION__ _T("(") fmt _T(")+\r\n"), __LINE__, __VA_ARGS__))
- #define FUNC_EXIT(fmt, ...) RETAILMSG(ZONE_FUNCTION, (__TMODULE__ _T(": -[") __TFILE__ _T(":%d] ") __TFUNCTION__ _T("(") fmt _T(")-\r\n"), __LINE__, __VA_ARGS__))
- #define LOGMSG(cond, fmt, ...) RETAILMSG(cond, (__TMODULE__ _T(": [") __TFILE__ _T(":%d] ") __TFUNCTION__ _T("> ") fmt, __LINE__, __VA_ARGS__))
- #else
- #define FUNC_ENTRY(fmt, ...) RETAILMSG(ZONE_FUNCTION, (__TMODULE__ _T(": +") __TFUNCTION__ _T("(") fmt _T(")+\r\n"), __VA_ARGS__))
- #define FUNC_EXIT(fmt, ...) RETAILMSG(ZONE_FUNCTION, (__TMODULE__ _T(": -") __TFUNCTION__ _T("(") fmt _T(")-\r\n"), __VA_ARGS__))
- #define LOGMSG(cond, fmt, ...) RETAILMSG(cond, (__TMODULE__ _T(": ") __TFUNCTION__ _T("> ") fmt, __VA_ARGS__))
- #endif
- /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
The interesting part is from line 165 to 173 where I define three macros that allow you to log messages in a good format (in my humble opinion):
- FUNC_ENTRY
- FUNC_EXIT
- LOGMSG
Usage:
Examples:
FUNC_ENTRY(_T("lpParameter = 0x%08X"), lpParameter);
Output:
(Note the '+' characters that indicate function entry)
ModuleName: +FunctionName(lpParameter = 0xA5A5A5A5)+
Usage:
Examples:
FUNC_EXIT(_T("return %s"), bRet ? _T("true") : _T("false"));
Output:
(Note the '-' characters that indicate function exit)
ModuleName: -FunctionName(return false)-
Usage:
Examples:
LOGMSG(ZONE_ERROR, _T("ERROR: Invalid parameter (dwParam = %d)\r\n"), dwParam);
LOGMSG(ZONE_WARNING, _T("WARNING: Could not reach IP %s\r\n"), to_T(inet_ntoa(IPAddress)));
Output:
ModuleName: FunctionName> ERROR: Invalid parameter (dwParam = 1234)
ModuleName: FunctionName> WARNING: Could not reach IP 192.168.1.1
To use the Debug Zones template in your projects you simply add DebugZones.cpp and DebugZones.h to your solution and call RETAILREGISTERZONES(hMod) from your entry point function:
{
FUNC_ENTRY(_T("hInstance = 0x%08X, dwReason = 0x%08X"), hInstance, dwReason);
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
{
// Disable thread creation/deletion calls into this function
DisableThreadLibraryCalls((HMODULE)hInstance);
// Register with debug subsystem
RETAILREGISTERZONES((HMODULE)hInstance);
break;
}
case DLL_PROCESS_DETACH:
{
break;
}
default:
break;
