Friday, April 17, 2009

In one of the mailing lists I'm on today, someone noted that on their Windows Mobile device the performance of GDI is much better in portrait mode than it is in landscape mode and was wondering why.  The reason is actually pretty simple.  Think of the image on the screen as just a contiguous stream of bytes (which is probably is).  The physical display needs to get that data and "paint" it.  Displays are engineered to essentially take in the data in a linear fashion, left to right, top to bottom.  So it's a pretty simple operation to just send out the framebuffer data to the display.

Now consider what happens if you want the display rotated 90 degrees. Remember, the display requires the data top to bottom, left to right, as it will show on the screen.  To get it into that state you have 2 options.  You can either A) rotate the data as you take it out of the frame buffer and send it to the display or B) rotate all calls to draw into the framebuffer.  In either case you have to do a matrix transform, which is expensive and gets worse the larger your display gets (so a VGA device will be more affected than a QVGA device).

Of course some devices have hardware acceleration that can greatly improve things by having matrix functions right in the silicon, but that still requires that the OEM actually modifies the driver to use that function (you'd be surprised how many don't).

So how bad is the performance penalty?  Well, since I like to actually quantify stuff I decided to resurrect an old GDI test app I had and rework it for this.  The results of my testing, as well as tests sent in from other people (if you want to add a test to the table, send me your results):

Device Processor OS Portrait Landscape Penalty
Axim x51 PXA270 WinMo 5 188 ops/s 77 ops/s 59%
Asus P750 PXA270 520MHz WinMo 6 Pro 241 ops/s 55 ops/s 77%

As you can see, there's a significant price to running rotated.

The test application (source and ARMv4I binary) is available here

GDIPerf.zip (10.46 KB)

 

4/17/2009 2:32:33 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1]  | 
 Wednesday, April 15, 2009

If you've ever needed to automatically launch an application under Windows CE then you probably know that one of the most common ways is to put an entry into the device registry under HKLM\Init.  This works great for native applications, but for CF applications success is hit or miss.  The reason is that CF applications require certain system APIs to be up and running before they can run.  For a native app that has this requirement, the solution is simple: you call either IsApiReady or WaitForAPIReady (depending on the OS version) and then continue when you're satisfied.  For CF apps it won't work.  The APIs need to be ready long before Main is entered and any of your managed code is running.

Of course the "right" solution is that the CF team should have put that check into the loader so we could launch this way, but they didn't so we have to work around it.

So how do we get around this?  Well we leverage the initialization process itself.  When an app is launched from HKLM\Init, it is responsible for calling SignalStarted once it is running.  This allows any other items launching from HKLM\Init to set up dependencies, for example if item 60 depends on item 50, item 60 won't launch until item 50 has called SignalStarted.  What we can do is create a native application that acts as a launch gate.  This gate app will call the appropriate wait function, and only after the APIs we need are available does it call SignalStarted.  We can then launch any CF application using HKLM\Init by simply having the gate application launch first, and then having the CF app depend on the gate.

So in the registry, it would look like this:

[HKEY_LOCAL_MACHINE\Init]
    "Launch90"="gateapp.exe"
    "Depend90"=hex:1e,00 ; depend on GWES, which is at 30
    "Launch91"="MyCFApp.exe"
    "Depend91"=hex:5a,00 ; depend on gateapp

And here's the source code for a basic gate app:

extern "C" DWORD WaitForAPIReady(DWORD, DWORD);
extern "C" BOOL IsAPIReady(DWORD hAPI);

int _tmain(int argc, _TCHAR* argv[])
{
    // quick sanity check - HKLM\Init will send in our order number
    if(argc == 0) return 0;

    BOOL success = FALSE;

    // wait for window manager - that should be enough for us
    #if _WIN32_WCE > 0x500
        success = (WaitForAPIReady(SH_WMGR, 5000) == WAIT_OBJECT_0);
    #else
        int i = 0;
        while((! IsAPIReady(SH_WMGR)) && (i++ < 50))
        {
             Sleep(100);
        }

        success = (i < 50);
    #endif

    if(success)
    {
        int launchCode = _ttoi(argv[1]);
        SignalStarted(launchCode);
    }
    else
    {
        RETAILMSG(TRUE, (_T("CFInitGate timed out - SH_WMGR was not ready after 5 seconds\r\n")));
    }

    return 0;
}

4/15/2009 1:46:38 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [2]  | 
 Friday, March 20, 2009

OpenNETCF used to have a product that provided RAS capabilities.  Well Microsoft deprecated RAS in WindowsMobile - starting with WinMo 5.0 as near as I can figure - in favor of Connection Manager.  That depracation actually broke RAS in some scenarios.  FOr example, if you create a RAS connection and then try to use a managed TcpClient, it will try to create it's own connection through the Connection Manager instead of using the connection you created through RAS.

What this did was skyrocket our support incidents for something that we sold very, very few licenses for.  In the interest of keeping our sanity, yet allowing people using plain of Windows CE to still use RAS, we made the decision to just make it a shared source project.  It's now out on CodePlex.  If you need support for it, we still offer consulting services, but everything is there to get you going.

3/20/2009 12:54:05 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Monday, May 19, 2008
We've published a new article on the OpenNETCF Community Site titled "Native vs. Managed Code: GDI Performance"
 
 
In it, I look at the performance differences between native and managed code making GDI calls.
 
In case you missed them, our other recently published articles include:
 
- Performance Implications of Crossing the P/Invoke Boundary
- An Introduction to WCF for Device Developers
- Getting a Millisecond-Resolution DateTime under Windows CE
- Using GDI+ on Windows Mobile
- Sharing Windows Mobile Ink with the Desktop
- OpenNETCF Mobile Ink Library for Windows Mobile 6
- Improving Data Access Performance with Data Caching
- Developing Connected Smart Device Applications with sqlClient
- Debugging Without ActiveSync
- Image Manipulation in Windows Mobile 5.0
- Don't Fear the Garbage Collector
 
All of our articles are available online at:
http://community.OpenNETCF.com/articles
 
5/19/2008 12:27:16 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Tuesday, October 30, 2007
OpenNETCF is proud to announce that we've released Padarn - an ASP.NET Web Server designed to run under the limited resource environment of Windows CE.  For more information on Padarn, visit our web site.

10/30/2007 12:51:03 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [2]  | 
 Tuesday, September 04, 2007

Today we were working on some final testing of a new app and I was irritated by the About Form's behavior of generice CE devices.  When we went to display the form, we'd get the ugly "expanding rectangle" animation of the Form opening, and even worse, when we closed it it would animate the close, then animate the re-opeing of the main form.

Unfortunately the Compact Framework give us absolutely nothing for controlling this behavior (hello?  Windows Mobile isn't the only platform the CF is used on....).  So we have to work around it.

Add the following to your form (or a utility class is what we actually used, as we called this from multiple places):

private const int WS_EX_NOANIMATION = 0x04000000;
private const int GWL_EX_STYLE = -20;

[DllImport("Coredll.dll", SetLastError=true)]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

[DllImport("coredll.dll", SetLastError=true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

internal static void StopAnimation(Form form)
{
    int style = GetWindowLong(form.Handle, GWL_EX_STYLE);
    style |= WS_EX_NOANIMATION;
    SetWindowLong(form.Handle, GWL_EX_STYLE, style);
}

Then simply call StopAnimation in your Form's constructor immediately after InitializeComponent is called:

public Form1()
{
    InitializeComponent();
    StopAnimation(this);
}

We'll probably add this to the next release of the Smart Device Framework.

9/4/2007 4:41:52 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Thursday, July 05, 2007

Not surprisingly, our initial release of the OpenTimeCE library (a utility to allow developers to port code using the time_t, mktime, etc APIs to Windows CE) has some bugs in it.  First releases are rarely bug free.  What was really nice was that these fixes were purely community driven.  We'd received some "reports" in the past that there were bugs, but finally today someone (thanks Ben Murdoch) sent in a fully patched source file, so we rolled it into the code base and re-released it. It just goes to show that an open-source mindset and a Microsoft product *can* indeed not just co-exist, but provide something beneficial.

 

7/5/2007 4:43:21 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1]  | 
 Tuesday, June 26, 2007

Like most developers, I have tons of small code samples and snippets lying around.  I'm starting to go through them in order to post what might be useful to others.

Here's one that I wrote about 5 years ago when I had a customer complaining about the speed of parsing an XML document using MSXML.  Of course this test simply confirmed that yes, MSXML is molasses-in-January slow and that if raw speed is what you want, then something like Expat is a better route to go.  However the test is a reasonably good sample of how you might use the DOMDocument from a C++ application.

As always, the code is an as-is sample.  uSe it at your own risk.

#include <windows.h>
#include "objbase.h"
#include "oleauto.h"
#include "MSXML.h"
#include "ATLBASE.h"
#import "msxml.dll" named_guids raw_interfaces_only

TCHAR *xmlfile = _T("\\test.xml");
using namespace MSXML;

void GetCurrentDirectory(TCHAR *szDirectory);


int WINAPI WinMain(    HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPTSTR lpCmdLine,
                    int nCmdShow)
{
    MSXML::IXMLDOMDocument      *iXMLDoc          = NULL;
    HRESULT                     hr;
    VARIANT_BOOL                b;
    int                         et                = 0;
    HANDLE                      hFile             = NULL;
    DWORD                       wsize, dwhigh     = 0;
    TCHAR                       filename[MAX_PATH];
    MEMORYSTATUS                ms;
    BYTE                        *buffer           = NULL;
    TCHAR                       *wbuffer          = NULL;
    DWORD                       dwRead            = 0;
    DWORD                       toalloc           = 0;
    int                         i;
    CComBSTR                    *bstr;

    _tprintf(_T("Starting XML Benchmark...\n"));
    _tprintf(_T("Creating DOMDocument..."));

    // start COM
    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

    // create a DOMDocument
    hr = CoCreateInstance (MSXML::CLSID_DOMDocument, NULL,
                 CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
                 MSXML::IID_IXMLDOMDocument,(LPVOID *)&iXMLDoc);

    // check for success
    if(!iXMLDoc)
    {
        _tprintf(_T("failed (hr = %i)\n"), hr);
        goto exit;
    }

    _tprintf(_T("ok\n"));

    // synchronous operation
    iXMLDoc->put_async(VARIANT_FALSE);

    // get the data file - pull from the same directory as our app
    GetCurrentDirectory(filename);
    _tcscat(filename, xmlfile);
    _tprintf(_T("Opening XML file (%s)..."), filename);

    hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);

    if(hFile == INVALID_HANDLE_VALUE)
    {
        _tprintf(_T("failed (err = %i)\n"), GetLastError());
        goto exit;
    }

    // determine the file size
    dwsize = GetFileSize(hFile, &dwhigh);

    _tprintf(_T("ok\n"));

    // check available memory
    ms.dwLength = sizeof(MEMORYSTATUS);
    GlobalMemoryStatus(&ms);

    // either 1/3 available memory or file size, whichever is less
    toalloc = (dwsize > (ms.dwAvailPhys / 3)) ? (ms.dwAvailPhys / 3) : dwsize;

    _tprintf(_T("Allocating %i bytes..."), toalloc);
    
    //alocate buffers. data is ANSI, loadXML requires Unicode
    buffer = (BYTE *)LocalAlloc(LPTR, toalloc);
    wbuffer = (TCHAR *)LocalAlloc(LPTR, toalloc * sizeof(TCHAR) + 1);

    // check for success
    if((buffer == NULL) || (wbuffer == NULL))
    {
        _tprintf(_T("failed (err = %i)\n"), GetLastError());
        goto exit;
    }

    _tprintf(_T("ok\nReading %i bytes..."), toalloc);
    
    // fill the RAM buffer with data
    ReadFile(hFile, &buffer[0], toalloc, &dwRead, NULL);

    // lazy man's ANSI to Unicode conversion
    // this is NOT recommended for production code, don't cut and paste!!
    for(i = 0 ; i < (int)toalloc ; i++)
    {
        wbuffer[i] = buffer[i];
    }
    wbuffer[i] = '\0';
    bstr = new CComBSTR(wbuffer);

    _tprintf(_T("ok\nLoading %i bytes into DOMDocument..."), toalloc);

    // get a start time
    et = GetTickCount();

    // load the xml
    hr = iXMLDoc->loadXML(*bstr, &b);

    // determine ET
    et = GetTickCount() - et;

    // check for load failure
    if(!b)
    {
        _tprintf(_T("failed (hr = %i)\n"), hr);
        _tprintf(_T("GetLastError = %i\n"), GetLastError());
        goto exit;
    }

    _tprintf(_T("done.\n\nET = %i ms\n\n"), et);

exit:
    // release the DOMDocument
    iXMLDoc->Release();

    // release the file
    CloseHandle(hFile);

    // release COM
    CoUninitialize();

    _tprintf(_T("Exiting XML Benchmark in 5 seconds...\n"));
    
    // free buffers
    if(buffer)
        LocalFree(buffer);
    
    if(wbuffer)
        LocalFree(wbuffer);

    // wait to allow display to persist for a while
    Sleep(5000);

    return 0;
}

void GetCurrentDirectory(TCHAR *szDirectory)
{
    TCHAR *p, *p2;

    // get current directory
    GetModuleFileName(NULL, szDirectory, MAX_PATH);

    // trim off the exe name
    p = _tcsstr(szDirectory, _T("\\"));

    if(p == NULL)
    {
        // no slash found - return root?
        _tcscpy(szDirectory, _T("\\"));
        
        return;
    }

    while( p != NULL)
    {
        p2 = p + 1;
        p = _tcsstr(p2, _T("\\"));
    }

    // null terminate to crop
    p2--;
    *p2 = '\0';

    return;
}

6/26/2007 4:16:45 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Friday, June 22, 2007

A newsgroup post today reminded me of a simple app I wrote a few years back that enumerates all Fonts on a CE device, then searches a specific location (an inserted storage card in this case), adds any fonts found, then re-enumerates all the existing fonts.

Here's the code, in its entirety:

#include <windows.h>

#define FONT_PATH    (_T("\\Storage Card"))

void FindFonts(TCHAR *location, TCHAR *type);
int CALLBACK EnumFontFamProc(ENUMLOGFONT *lpelf, TEXTMETRIC *lpntm,
                             int FontType, LPARAM lParam);

int WINAPI WinMain(
HINSTANCE hInst,
HINSTANCE hInstPrev,
LPTSTR lpszCmdLine,
int nCmdShow)
{
    HDC    pDC = GetDC(NULL);
    //
    // Enumerate all the fonts
    //
    RETAILMSG(TRUE, (_T("\r\n------------------------------\r\n")));
    RETAILMSG(TRUE, (_T("Available Fonts on this device\r\n------------------------------\r\n")));
    EnumFontFamilies(pDC, NULL, (FONTENUMPROC)EnumFontFamProc, NULL);
    RETAILMSG(TRUE, (_T("------------------------------\r\n\n")));

    RETAILMSG(TRUE, (_T("Looking for fonts on Storage Card...\r\n")));


    // TrueType file (.ttf)
    FindFonts(FONT_PATH, _T("TTF"));
    // TrueType resource file (.fot)
    FindFonts(FONT_PATH, _T("FOT"));
    // TrueType collection file (.ttc)
    FindFonts(FONT_PATH, _T("TTC"));
    // raw bitmap font file (.fnt)
    FindFonts(FONT_PATH, _T("FNT"));
    // raster resource file (.fon)
    FindFonts(FONT_PATH, _T("FON"));


    RETAILMSG(TRUE, (_T("\r\n------------------------------\r\n")));
    RETAILMSG(TRUE, (_T("Available Fonts on this device\r\n------------------------------\r\n")));
    EnumFontFamilies(pDC, NULL, (FONTENUMPROC)EnumFontFamProc, NULL);
    RETAILMSG(TRUE, (_T("------------------------------\r\n\n")));

    ReleaseDC(NULL, pDC);

    return 0;
}

void FindFonts(TCHAR *location, TCHAR *type)
{
    TCHAR fontpath[MAX_PATH];
    TCHAR findpath[MAX_PATH];

    WIN32_FIND_DATA fd;

    _stprintf(findpath, _T("%s\\*.%s"), location, type);

    HANDLE hFind = FindFirstFile(findpath, &fd);
    
    if(hFind != INVALID_HANDLE_VALUE)
    {
        RETAILMSG(TRUE, (_T(" found %s\r\n"), fd.cFileName));
        _stprintf(fontpath, _T("\\Storage Card\\%s"), fd.cFileName);

        AddFontResource(fontpath);

        while(FindNextFile(hFind, &fd))
        {
            RETAILMSG(TRUE, (_T(" found %s\r\n"), fd.cFileName));
            _stprintf(fontpath, _T("\\Storage Card\\%s"), fd.cFileName);
            AddFontResource(fontpath);
        }
    }
    FindClose(hFind);
}

int CALLBACK EnumFontFamProc(ENUMLOGFONT *lpelf, TEXTMETRIC *lpntm,
                             int FontType, LPARAM lParam)
{
    RETAILMSG(TRUE, (_T("\t%s\r\n"), lpelf->elfFullName));

    return 1;
}

6/22/2007 1:50:03 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Friday, April 06, 2007

If you've done much development on non-Windows Mobile CE devices I'm sure your painfully aware of the pain in the ass process of getting Visual Studio 2005 to actually attach to the device.  Lately I've been doing nothing but non-WM development and deviced to automate and solidify the process, plus document it while I was at it.

We've got a (native) tool called CEDbgSetup that you run on your device (x86 or ARMv4I supported) and then you set up Studio once and only once, then you can debug every time using wired Ethernet, 802.11, USB RNDIS (and likely any other transport that uses TCP/IP) and without ActiveSync.

The tool supports auto-launching from \Windows\Startup or by setting up an LaunchXX entry in HKLM\Init in the device registry, or just manual click.  It should work headless too, though I've not specifically tested it.

It works with native or managed apps.

The tool, along with full source can be downloaded here (607k zip).

A white paper on how to set up Visual Studio 2005 can be viewed here (144k PDF).

Feedback is appreciated, but keep in mind that its free and completely unsupported (read: don't ask me for support).

4/6/2007 5:49:54 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [2]  | 
 Thursday, March 01, 2007

I've been writing apps in mostly C# for several years now.  Of course I've also been doing drivers and kernel work in C or C++, and that has only improved my ability to write, debug and understand managed code in ways that pure managed developers will never really get.  Hell, I pretty much believe that if your managed code doesn't require allowing unsafe code then it could probably be done better.

Well for a few months now I've been neck-deep in a C++ application.  It was started using MFC, which tends to give me a rash just by saying the letters, but it was a customer decision not mine.  At any rate, I was doing some work with a RAS class where I needed any number of consumers to be able to be notified of any changes in the RAS dial status (dialing, connecting, authenticating, etc.).

Imagine a C# class for RAS that will expose events for when the dial staus changes.  THe first thing you'd need is a delegate, right?

In C# you'd have this (assuming DialStatus is an enum):

delegate void DialStatusChange(DialStatus dialStatus);

Well the c++ I wrote (without even thinking about C#) looked eerily familiar:

typedef void(*DIAL_STATUS_DELEGATE)(DialStatus newStatus);

In C# you'd use the += or -= operators for adding an event handler to a classes event, so we get spoiled.  In VB, you call AddHandler with the event name and then the address of the handler.  Well my C++ had a private vector of function pointers (which the CF maintains internally):

std::vector <DIAL_STATUS_DELEGATE> ConnectionManager::m_statusCallbackList;

And then I added methods for adding and removing handlers to the class:

 void ConnectionManager::StatusAddHandler(DIAL_STATUS_DELEGATE callback)
 {
   m_statusCallbackList.push_back(callback);
 }
 
 void ConnectionManager::StatusRemoveHandler(DIAL_STATUS_DELEGATE callback)
 {
   std::vector<DIAL_STATUS_DELEGATE>::iterator iterator;
 
   for(iterator = m_statusCallbackList.begin() ; iterator != m_statusCallbackList.end() ; iterator++)
   {
     if((*iterator) == callback)
     {
        m_statusCallbackList.erase(iterator);
        break;
     }
   }
 }

And how about usage?  In C#, our event-exposing class would have a defined event:

 event DialStatusChange OnStatusChange;

and then when we want to raise the event (assume it's multicast) from our app, we'd do something like this:

 if (OnStatusChange != null)
 {
     foreach (DialStatusChange dsc in OnStatusChange.GetInvocationList())
     {
         dsc(newStatus);
     }
 }

Well my C++ didn't need the event declaration, but raising the "event" (which is simply calling a function pointer callback) looked like this:

 std::vector<DIAL_STATUS_DELEGATE>::iterator iterator;
 
 recheck2:
 for(iterator = m_statusCallbackList.begin() ; iterator != m_statusCallbackList.end() ; iterator++)
 {
   if(IsBadCodePtr((FARPROC)(*iterator)))
   {
      // invalid callback found (someone hooked us then died without unhooking)
      m_statusCallbackList.erase(iterator);
      goto recheck2;
   }
      (*iterator)(status);
 }

Now this was just off the top of my head, so maybe there are improvements that could be made, but it kind of surprised me how I'd taken the concepts I really learned in C# and translated them back to my C++ code.  So yes, writing C# can make you a better C++ developer.

3/1/2007 12:02:32 AM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Thursday, February 22, 2007

For all those naysayers who complain that we don't do enough free stuff for the community, here's another contribution:

We're now hosting the latest zlib source for Windows CE and a managed wrapper for it.  Click the bitmap or here for more info.

2/22/2007 11:49:30 AM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Wednesday, February 07, 2007

Some days I really want to strangle Microsoft engineers.

 

For the past couple days I've been working on a project where I want a CE device to dial in to an XP desktop. In a perfect world, I'd just use RAS, create an entry, and go.  Well this isn't a perfect world.

 

Spent several hours yesterday trying different chipsets and PCs to finally determine that my device modem seems to be not-so-good and it won't negotiate at the default 56kbps that the chipset uses (than you Alex Feinman for the help).

 

The workaround is to manually slow it down with an AT command.  Using terminal on the device I just type it in and boom - it's set.  Next step was to try to repro that simple operation in code.  This is where the fun begins. Well this extra dialing data is set via the RasSetEntryProperties API in the lpb parameter.  The parameter docs say "Pointer to a buffer that contains device-specific configuration information. This is opaque TAPI device configuration information." 

 

It gives a reference to the TAPI lineGetDevConfig API for getting that data.  So we look over there and fine that it's a VARSTRING parameter.  Simple enough eh?  I fill in a VARSTRING with the command and send it in.  RasSetEntryProperties returns a success.

 

For fun I then open the connection via the UI's Network Connections.  Click on the "Configure" button and the shell completely locks up.  That can't be good.

 

So I figure, I'll see what the UI generates and use that to figure out what I screwed up.  I create a connection entry manually and add the info into the dialog.  Now clicking "Configure" works fine, so it's back to the code.

 

I call RasGetEntryProperties on the manually created entry to see what it looks like.  I see my data's in there, but it certainly isn't aligned as a VARSTRING and I have no idea what the "header" data is.  The length reported is also *way* bigger than my data.

 

  0x001291C0  30 00 00 00 78 00 00 00 10 01 00 00 00 4b 00 00  0...x........K..

  0x001291D0  00 00 08 00 00 00 61 00 74 00 2b 00 6d 00 73 00  ......a.t.+.m.s.

  0x001291E0  3d 00 76 00 33 00 32 00 2c 00 2c 00 31 00 34 00  =.v.3.2.,.,.1.4.

  0x001291F0  34 00 30 00 30 00 2c 00 32 00 38 00 38 00 30 00  4.0.0.,.2.8.8.0.

  0x00129200  30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  0...............

  etc.

 

Alright, so what's going on here?  Off to the Platform Builder source (and this is why every developer should get the PB eval and install it - the source is gold) to see how the default UI is doing it.  A little GREP on the dialog text and I find that the UI handling this is, not surprisingly NETUI.

 

So after a long while tracing through convoluted code (Source Insight is an invaluable tool here) it appears that you're supposed to call lineConfigDialogEdit and have TAPI give a dialog to fill this structure.  Well what if you don't want user interaction (like in my case where the user would have no idea what to enter)?  Or even more fun, what if it's a headless device?

 

Ok, so time to regroup.  Let's search all the PB source for anything calling lineSetDevConfig to see if there's something to base my work on.  Turns out that there are a few samples, but most of them just do a lineGetDevConfig, save that data somewhere, then send it back in - they don't actually modify it.

 

One exception is the sample RASENTRY app.  It calls lineGetDevConfig, and then appears to alter that config not through lineSetDevConfig but instead by calling lineDevSpecific.  It uses this to change the baud rate.  It's not exactly what I need (I need to prevent the modem from negotiating a v.90 or v.92 connection) but a start, right?

 

So a little work and I ended up with this (well more than this, but it's the heart of it):

 

            result = lineGetDevConfig(tapiId, config, DEV_CLASS_COMM_DATAMODEM);

            ucd.dwCommand = UNIMDM_CMD_CHG_DEVCFG;

            ucd.lpszDeviceClass = DEV_CLASS_COMM_DATAMODEM;

            ucd.lpDevConfig = config;

            ucd.dwOption = UNIMDM_OPT_CFGBLOB;

            ucd.dwValue = (DWORD)MY_CUSTOM_DIALUP_STRING;

            result = lineDevSpecific(hLine, 0, NULL, &ucd, sizeof(ucd));

 

I run it and all APIs return success.  Hooray!  Right?  Open the connection with the UI on the device and the "Extra Settings" box is still blank.  At this point I have to chalk it up to a huge WTF in the Microsoft code.

 

At this point I have no option but to go the route that the UNIMODEM dialer went (again in PB source) and set it in the registry at HKEY_LOCAL_MACHINE\Drivers\Unimodem\Init.  Some may argue that this is easier and I should have gone that route in the first place, but that just doesn't sit well with me and not just because it affects all Unimodem connections on the device. 

 

I strive to understand what is going on when I'm writing code because it not only makes it easier to debug aberrant behavior but it also helps make me a better developer all around. I hate black-box programming - that is programming where I don't fully understand how something works from end to end.  Somehow TAPI gets that data into the blob returned by RasGetEntryProperties, but I'll be damned if I can figure it out. 

 

The actual blob is created or copied to  HKCU\Comm\RasBook\<connection name>\DevCfg and it appears to be an extension of HKLM\Drivers\Unimodem\DevConfig, but they are slightly different and what those differences mean is totally undocumented.

2/7/2007 3:18:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Tuesday, January 02, 2007

Sue Loh posted a really good blog entry on what seems to confuse many people - what exactlyis an SDK.  While her entry is really targeted toward the native developer, it's also quite applicable to a managed developer as well.

1/2/2007 8:04:38 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Tuesday, December 19, 2006

Over the years I've noticed that questions always come in groups.  When I worked at Applied Data Systems, when a support question came in we always went the extra mile in writing up an answer because invariably we'd get at least 2 more questions on the same topic within a week.

I still see the same thing today.  Last week a fried who is pretty new to C development asked me about returning a string from a function and I went through and explained the hows and whys.  Then today I see the same question in a newsgroup.  Rather than just provide a short answer, I figure I might as well head off the repeats that are sure to come up in the coming weeks by providing a reasonable blog entry on it so I can just post a link.

So, how do we return a string from a C or C++ function anyway?  Well the short answer is "you don't."  You can, but it's rare that you ever should.  The two exceptions that come to mind are if you have an accessor that returns a globally allocated value, like a constant, or if the return is simply a copy or internal location of a parameter to the same function (strstr is an example).  Let's look at the reason why this is the case.

Let's create a method to get a string:

TCHAR *GetString()
{
}

Now the question here is memory ownership.  For the function to return some string value, a buffer must be allocated to hold that value.  So let's add that:

TCHAR *GetString()
{
    TCHAR *value = (TCHAR*)LocalAlloc(LPTR, MAX_PATH);
    _tcscpy(value, _T("Hello memory leak"));
    return value;
}

So now we allocate a buffer and return it.  This will work, fine but the caller then must know that after they use the string they must call LocalFree or they'll have a memory leak.  Now if you're tempted to say "yes, but I'll remember that" or "yes, but it's in an internal library that I'll use LocalFree in and it will never change" then you haven't been developing long.  Rule #1 is that code will *always* be changed, or copied into another project.  Rule #2 is that it is almost always someone else who will do it. Even if you're the one that does it, believe me, you won't remember this 2 years down the road until you've burned 2 weeks trying to find the memory leak that a high-profile customer is complaining about and that management has made priority 1 for the entire team.  It happens.  Do not be tempted to do this.  Ever.

Ok, so we all agree that returning a string is bad (nod your head - yes you agree).  So how do we do it?  Well we know that to prevent a leak, the caller needs to do the allocation, so can't we just pass in the buffer as a pointer?  How about this?

void GetString(TCHAR *value)
{
    _tcscpy(value, _T("Hello overrun"));
}

The value I used should be a clue as to what the problem here is.  Let's look at a use case.

TCHAR *myValue = (TCHAR*)LocalAlloc(LPTR, MAX_PATH);
GetString(myValue);

Will this work?  Sure, in this exact case it will.  But what if GetString's value is larger than MAX_PATH?  What if the caller allocated a smaller buffer, or didn't allocate one at all?  Well you'll get a buffer overrun.  If you're lucky this will manifest as a first chance exception or something that blows up spectacularly and is easy to find.  If you're not lucky (and if you're against a tight deadline, you won't be) it will cause unexpected and non-reproducible behavior that is a real bitch to debug (on a Palm I've seen this cause execution to jump right to another application with no warning - try debugging that). 

So this could be even worse than our original FUBAR (if you're unfamiliar I'll let you look it up) code.  So what's the right way to do this?  Well a good indication is how the Win32 APIs work.  Look at something like RegQueryValue.  It takes a buffer pointer and a size. And if the function is really nice, it will tell me how big the buffer should be if the caller has it too small.  Let's look at an example.

BOOL ReturnString3(TCHAR *myString, DWORD *size)
{
    DWORD requiredSize = 0;
    TCHAR *buffer = _T("Hello nice function");

    // determine how big the buffer must be
    requiredSize = _tcslen(buffer);

    if(IsBadWritePtr(myString, requiredSize)
    {
        *size = requiredSize;
        SetLastError(ERROR_INSUFFICIENT_BUFFER);
        return FALSE;
    }

    _tcscpy(myString, buffer);
    *size = _tcslen(myString);
    return TRUE;
}

Here you see that we first determine how big the buffer should be and then we check the incoming buffer against that number.  If it fails, we copy the required size to the size parameter, set a meaningful error, and return FALSE so the caller knows we failed (we can't force them to read the return, but if they don't that's just poor practice).

If the buffer is big enough, we copy in the value, set the actual size (in case they want it for something) and return TRUE.

A use case would look like this:

TCHAR *getString = NULL;
DWORD size = 0;

// this is supposed to fail - it gets the size
ReturnString3(getString, &size);

// now allocate a buffer
getString = LocalAlloc(LPTR, size);

// and get the data
BOOL success = ReturnString3(getString, &size);

So now you've seen the good, the bad and the ugly on how to return a string froma C function.  I have no illusions that me posting this will stop people from asking - even if it ends up as the number 1 result for Google.  Let's face it, you have to know enough to do the search for it to be of any use.  But hopefully, those who are in the industry because they like to learn and don't like to write crap code can either find it or be pointed to it and make good use of it.

12/19/2006 9:58:42 AM (Eastern Standard Time, UTC-05:00)  #    Comments [2]  | 
 Monday, December 11, 2006

Over the last week of so I've noticed several questions in the newsgroups about how to synchronize a CE device's time with a time server.  Once again, it's not something I'd done, but I knew there were servers out there that provide the time publicly, so it couldn't bee too tough.

First stop was to see if Wikipedia gave a simple explanation.  Useful, but not exactly what we need - we want to know exactly what the server expects, and what exactly it returns.  It does give us the RFCs.

So a little looking at RFCs and we see that RFC 2030 is very applicable, and gives us all the info we need about the protocol.

A little more looking with Google found a public server domain name from NTP.org that actually rotates through public NTP servers, so we don't have to worry about one being up or not.

Armed with nothing but the second two links I wrote a little code.  This could be far more robust, with fractional seconds. mode, stratum, precision info and all that, but I just wanted to get a reasonable time - so to the second is all I was after.

public DateTime GetNTPTime()
{
   // 0x1B == 0b11011 == NTP version 3, client - see RFC 2030
   byte[] ntpPacket = new byte[] { 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

   IPAddress[] addressList = Dns.GetHostEntry("pool.ntp.org").AddressList;

   if (addressList.Length == 0)
   {
      // error
      return DateTime.MinValue;
   }

   IPEndPoint ep = new IPEndPoint(addressList[0], 123);
   UdpClient client = new UdpClient();
   client.Connect(ep);
   client.Send(ntpPacket, ntpPacket.Length);
   byte[] data = client.Receive(ref ep);

   // receive date data is at offset 32
   // Data is 64 bits - first 32 is seconds - we'll toss the fraction of a second
   // it is not in an endian order, so we must rearrange
   byte[] endianSeconds = new byte[4];
   endianSeconds[0] = (byte)(data[32 + 3] & (byte)0x7F); // turn off MSB (some servers set it)
   endianSeconds[1] = data[32 + 2];
   endianSeconds[2] = data[32 + 1];
   endianSeconds[3] = data[32 + 0];
   uint seconds = BitConverter.ToUInt32(endianSeconds, 0);

   return (new DateTime(1900, 1, 1)).AddSeconds(seconds);
}

12/11/2006 10:59:10 AM (Eastern Standard Time, UTC-05:00)  #    Comments [4]  | 
 Thursday, December 07, 2006

I was doing a search for some info just now and came across a really well done article describing drivers in Windows CE.  Check it out.

12/7/2006 6:17:45 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Saturday, February 18, 2006

I'm trying to debug an error in some managed code related to timezones when a P/Invoke is throwing a native access violation exception.  To make sure it's not something with the platform itself I decided to do the same in native code first to ensure that it's not some platform limitation or bug.  Surprisingly I didn't readily find any sample code for doing it, so here's my contribution to the community at large.  It uses dynamic loading (and it doesn't make sure that the function loads succeed, so you might want to add error checking if you actually intend to use this):

#include <windows.h>

typedef void    (*INITCITYDB)(void);
typedef void    (*UNINITCITYDB)(void);
typedef void    (*LOADTZDATA)(void);
typedef void    (*FREETZDATA)(void);
typedef int        (*GETNUMZONES)(void);
typedef void *    (*GETTZDATABYOFFSET)(int, int*);
typedef void *    (*GETTZDATA)(int);

struct TZData
{
    TCHAR *Name;
    TCHAR *ShortName;
    TCHAR *DSTName;
    int GMTOffset;
    int DSTOffset;
};

int _tmain(int argc, _TCHAR* argv[])
{
    TZData *pTZ = NULL;
    int index;

    // load the library
    HINSTANCE hLib = LoadLibrary(_T("CityDB.dll"));

    // load the CityDB functions
    INITCITYDB InitCityDB = (INITCITYDB)GetProcAddress(
            hLib, _T("InitCityDb"));
    UNINITCITYDB UninitCityDB = (UNINITCITYDB)GetProcAddress(
            hLib, _T("UninitCityDb"));
    LOADTZDATA ClockLoadAllTimeZoneData = (LOADTZDATA)GetProcAddress(
            hLib, _T("ClockLoadAllTimeZoneData"));
    FREETZDATA ClockFreeAllTimeZoneData = (FREETZDATA)GetProcAddress(
            hLib, _T("ClockFreeAllTimeZoneData"));
    GETNUMZONES ClockGetNumTimezones = (GETNUMZONES)GetProcAddress(
            hLib, _T("ClockGetNumTimezones"));
    GETTZDATABYOFFSET ClockGetTimeZoneDataByOffset = 
            
(GETTZDATABYOFFSET)GetProcAddress(hLib, _T("ClockGetTimeZoneDataByOffset"));
    GETTZDATA ClockGetTimeZoneData = (GETTZDATA)GetProcAddress(
            hLib, _T("ClockGetTimeZoneData"));

    // Init the library
    InitCityDB();

    // load the TZ data
    ClockLoadAllTimeZoneData();

    // find out how many zones are defined
    int zoneCount = ClockGetNumTimezones();

    // interate through them all
    for(int zone = 0 ; zone < zoneCount ; zone++)
    {
        // these are pointers to a timezone data struct
        pTZ = (TZData*)ClockGetTimeZoneDataByOffset(zone, &index);
    }

    // unload the TZ data
    ClockFreeAllTimeZoneData();

    // uninit the library
    UninitCityDB();

    return 0;
}

2/18/2006 1:45:07 PM (Eastern Standard Time, UTC-05:00)  #    Comments [3]  | 
 Saturday, January 21, 2006

We're extremely close to a beta release of SDF 2.0.  To give you a taste of what's in it, we've posted the online documentation.  Any feedback is appreciated.  Enjoy.

1/21/2006 11:49:17 PM (Eastern Standard Time, UTC-05:00)  #    Comments [3]  | 
 Thursday, December 08, 2005

If you're seeing stange behavior in Media Player under CE 4.2, this might be of interest.

Symptom:

I have an MPEG4 QVGA file (WMV file extension) recorded with a bitrate of about 1000kbps.  When I play the video on the desktop everything is just fine.

When I play it on the device, bot the audio and video play, but the video "lags" the audio, and not by a small amount.  It's an incremental loss, so they start out fine, but over time the video get further and further behind the audio which plays at what sounds like a normal rate.

For example, when I play off of a CF card, by the time the audio is at 0:30 in, the video is only showing the frames from about 0:15.  You read that right, it's like the video is playing at about half speed.  If I play directly from RAM, the lag is about 30-40%.

Some other observations:

  • The "progress" bar on Media Player tracks with the audio, so when I play a 3:30 video, then the timer says 3:30, the audio finishes, yet the video continues to play, and from CF it will play for over another minute.  There's not enough memory on this thig for it to be buffering that entire minute of video somewhere.
  • If I pause playback at some point where the audio and video are out of sync, then restart, both continue from where they left off, still out of sync.
  • If I stop the video, drag the progress slider to some point in the middle of the video and start it, the audio and video start synchronized at that point and begin diverging immediately.
  • If I add a load to the system by holding my finger on the touchpanel and wiggling the media player window around, thereby causing lots of interrupts, the lag amount does not change at all - so it seems load on the system doesn't affect how far off it gets.
  • If I resize the window, making the video eitehr much larger (out to VGA) or much smaller than the 320x240 it was recorded at, the lag does not change.
  • The video plays fine on my WM5.0 device with a PXA270 (no graphics accelerator) off of SD, smooth video and audio.
  • The video plays ok on my Pocket PC 2003SE device from CF or SD.  The audio is smooth, but the video is a bit choppy from dropped frames.  The important thing though is that even with that, the video and audio are still in sync.  That's how I thought Media Player was supposed to work and would be the behavior I'd expect on the custom hardware.

Explanation from Microsoft

“The problem was rooted from a frame dropping algorithm developed in Windows CE media player before 5.0. We have fixed correspondent bugs in CE 5.0 regarding this issue. So, from 5.0, CE media player does not have this problem.

If the HW resource is plentiful, e.g. faster CPU and memory bus speed (270 compared to 255), there would not be any frame drop. So, there won't be any A/V sync problem, as you see on the 270 and desktop.

When you read data from a memory card (depending on how fast your card is), or display high bitrate video (e.g. 1mbps QVGA for 255), or on a slow CPU, CE Media Player on 4.2 could potentially trigger this frame dropping mode and therefore exhibit the A/V sync issue when dropping frames.

Windows CE Media Player and PocketPC Media Player use a different code base because CE Media Player needs to support a variety of HW platforms while PPC media player focuses on a single environment. This issue of A/V sync when dropping frames does not exist in the PocketPC Media Player code base.”

12/8/2005 6:07:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Saturday, October 29, 2005

If you've not heard here are a few bits:

  1. Studio 2005 RTM is available for download for those with MSDN subscriptions.  If you've got a subscription, don't wait until the 7th, go get it now!
  2. CF 2.0 distributables are available here
  3. A Platform Builder 5.0 QFE for CF 2.0 is available here.
10/29/2005 10:11:41 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Thursday, September 15, 2005

So I just posted a blog entry about debugging and lo and behold, not 30 minutes later I his an exception in my platform.  I've done this tracing before, but this time I decided I'd document my discovery process and post it here as a real-world example.  It turned out to be an interesting example that I'd not hit before, so all the more fun to post.

So here's the background: I have a CE 4.2 PXA255-based system.  When I go to sleep and then wake back up with a USB device (keyboard, mouse or mass torage device) inserted I get the following exception:

RaiseException: Thread=81a8db58 Proc=816e7490 'device.exe'
AKY=00000005 PC=03fd3f80 RA=800bd398 BVA=00000001 FSR=00000001

This exception only happens at wake - if the device is in during boot there's no problem.

Nicely, when PB makes an image, it dumps the output into a PLG file in the same folder as your WCE and PBW files.  This output provides the map of where everything gets assembled. 

So let's take a look at what my exception is telling me.  I like to start with the return address (RA in the exception output) to see who the caller is causing the problem.  From Sue's entry, we can tell immediately it's in the kernel because the address is after 80000000. 

We need to find the offset (plus verify this assumption) so we'll look at the build output.Here's an excerpt from my myproject.plg file:

MODULES Section
Module                 Section  Start     Length  psize   vsize   Filler
---------------------- -------- --------- ------- ------- ------- ------
nk.exe                 .text    800b9000  258048  255488  255440 o32_rva=00001000
nk.exe                 .pdata   800f8000    8192    8192    7872 o32_rva=0006a000
cplmain.cpl            .text    800fa000  110592  109056  108976 o32_rva=00001000
cplmain.cpl            .rsrc    80115000  126976  126464  126276 o32_rva=0001f000

We can see that our return address of 800bd398 is in nk.exe at offset 0x4398 (0x800db398-0x800b9000).

Now we look in our _FLATRELEASEDIR and pull out the MAP file for NK.EXE.  This tells us the offsets for the beginnings of every function in the module (keep an eye on statics listed at the bottom of the map file).  Here's an excerpt from NK.MAP:

 0001:000041f8       UndefException             8c5851f8     nk:armtrap.obj
 0001:00004218       SWIHandler                 8c585218     nk:armtrap.obj
 0001:0000421c       FIQHandler                 8c58521c     nk:armtrap.obj
 0001:00004238       PrefetchAbortEH            8c585238     nk:armtrap.obj
 0001:00004240       PrefetchAbort              8c585240     nk:armtrap.obj
 0001:000044bc       MD_CBRtn                   8c5854bc     nk:armtrap.obj
 0001:00004578       CommonHandler              8c585578     nk:armtrap.obj
 0001:00004584       SaveAndReschedule          8c585584     nk:armtrap.obj

My calculated offset of 0x4398 is after the start of PrefetchAbort and before MD_CBRtn, so my exception is being throw by PrefetchAbort.  That surprises me - why the hell would PrefetchAbort be throwing an exception?  Even better, why is my code in PrefetchAbort in the first place?

Let's see where the exception is coming from exactly by chasing the program counter (PC=03fd3f80) from the exception.  Again we trun to our PLG file.  Here's an excerpt:

Module coredll.dll   at offset 01fff000 data, 03f71000 code
Module regenum.dll   at offset 01ffd000 data, 03f61000 code
Module pm.dll        at offset 01ffb000 data, 03f51000 code

Based on this we can see that our program counter address of 0x03fd3f80 is in coredll.dll at offset 0x62f80.  This is odd as I'd expect it to be in phci.dll or device.exe.  It appears that maybe we're passing some invalid info to a Win32 API, which then causes it to throw an exception?  Let's do more digging.

We go back to our _FLATRELEASEDIR and open up COREDLL.MAP to find what function in there is the culprit.  Here's an excerpt:

 0001:00062e40       __fp_mult_uncommon         10063e40     coredll_ALL:mul.obj
 0001:00062f5c       __rt_div0                  10063f5c f   coredll_ALL:__div0.obj
 0001:00062f90       __ld12mul                  10063f90 f   coredll_ALL:tenpow.obj

Ahhh...now things come together.  An offset of 0x62f80 puts us in a function called “__rt_div0”, which I'm going to assume is a runtime divide by zero exception.  That explains why we see an RA leading to PrefetchAbort.  Something in the platform code is causing a divide by zero error, which then causes PrefetchAbort to be called, which in turn enters __rt_div0.

So now what?  I still don't know what library or function caused the exception, and herein is a flaw in trying to track problems with addresses.  In this case (and this is the first I've seen this happen) I've got almost nothing.  I know it's in the USB host driver simply because of the  physical effects (happens only with USB devices, and when it happens the device stops working).  I know it's a divide by zero error.  That's all I get from this entire exercise - I wish it was a prettier case study that led me right to the line of code causing it, but hey, if it were simple everyone would do it.

9/15/2005 2:40:42 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 

Everyone that's done much development in CE knows about Doug Boling's definitive article on CE memory management.  If you've not read it, you should - if you have read it it doesn't hurt to read it again.  Go ahead and click the link - I'll wait here for you....

Done reading?  Good.  Now let's take a deeper look through the magic of blog voyeurism (yes, I'm not giving any info here other than links - I'm lazy).  Back in February, John Eldridge talked about how to tell if a debug symbol is even correctly being reported, and it good info.  Again, I'll wait while you read....

Now this got Sue Loh to thinking about what he said and a couple days later she put together a fantastic blog entry on making sense virtual addresses.  This one is short, but the info in it is invaluable if you do any amount of debugging at all.  No clue how I missed it until today, but it's one that every CE developer should read.

She then followed that up with an entry that shows how to go from an address to a symbol, allowing you to make use of that cryptic crap that Data Aborts give.

So there you go, links to some of the most valuable debugging information I've probably ever read, all in one place.  If you have other good debugging links, post them in the comments.

9/15/2005 11:50:27 AM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Friday, May 27, 2005

I was just saying that the SPOT stamp price would have to come down to be competitive.  As if to underscore that idea, look at what you can get for $100.  And that's a PXA255 with flash!  Heck for $120 you get bluetooth.  WEDIG is starting a project called GumSTIX to run CE on it.  This is way cool.

5/27/2005 6:29:14 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1]  | 
 Tuesday, February 08, 2005

A question came up in todays CF chat about authenticating a CE device to a domain.  While I don't have ready-made CF code for it (though that might be an interesting task) here's a C version:

#include <WINDOWS.H>
#include <SECURITY.H>
#include <SSPI.H>
#define SEC_PACKAGE _T("Microsoft Unified Security Protocol Provider")
typedef struct _AUTH_SEQ {
   BOOL fInitialized;
   BOOL fHaveCredHandle;
   BOOL fHaveCtxtHandle;
   CredHandle hcred;
   struct _SecHandle hctxt;
} AUTH_SEQ, *PAUTH_SEQ;
BOOL GenClientContext(PAUTH_SEQ pAS, PSEC_WINNT_AUTH_IDENTITY pAuthIdentity,
      PVOID pIn, DWORD cbIn, PVOID pOut, PDWORD pcbOut, PBOOL pfDone);
BOOL GenServerContext(PAUTH_SEQ pAS, PVOID pIn, DWORD cbIn, PVOID pOut,
      PDWORD pcbOut, PBOOL pfDone);
BOOL WINAPI SSPLogonUser(LPTSTR szDomain, LPTSTR szUser, LPTSTR szPassword);

TCHAR *lpszUserName = _T("ctacke");
TCHAR *lpszDomainName = _T("mydomain");
TCHAR *lpszPassword = _T("mypassword");

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR lpCmdLine, int nCmShow)
{
 return SSPLogonUser(lpszDomainName, lpszUserName, lpszPassword);
}
BOOL GenClientContext(PAUTH_SEQ pAS, PSEC_WINNT_AUTH_IDENTITY pAuthIdentity,
      PVOID pIn, DWORD cbIn, PVOID pOut, PDWORD pcbOut, PBOOL pfDone) 
{
 DEBUGMSG(TRUE, (_T("+GenServerContext\r\n")));
 SECURITY_STATUS ss;
 TimeStamp       tsExpiry;
 SecBufferDesc   sbdOut;
 SecBuffer       sbOut;
 SecBufferDesc   sbdIn;
 SecBuffer       sbIn;
 ULONG           fContextAttr;
 if (!pAS->fInitialized) 
 {
  DEBUGMSG(TRUE, (_T("Calling AcquireCredentialsHandle...")));
  ss = AcquireCredentialsHandle(NULL, SEC_PACKAGE, 
   SECPKG_CRED_OUTBOUND, NULL, pAuthIdentity, NULL, NULL,
   &pAS->hcred, &tsExpiry);
  
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  if(ss < 0) 
  {
   fprintf(stderr, "AcquireCredentialsHandle failed with %08X\n", ss);
   return FALSE;
  }
  pAS->fHaveCredHandle = TRUE;
 }
   // Prepare output buffer
   sbdOut.ulVersion = 0;
   sbdOut.cBuffers = 1;
   sbdOut.pBuffers = &sbOut;
   sbOut.cbBuffer = *pcbOut;
   sbOut.BufferType = SECBUFFER_TOKEN;
   sbOut.pvBuffer = pOut;
   // Prepare input buffer
   if (pAS->fInitialized)  {
      sbdIn.ulVersion = 0;
      sbdIn.cBuffers = 1;
      sbdIn.pBuffers = &sbIn;
      sbIn.cbBuffer = cbIn;
      sbIn.BufferType = SECBUFFER_TOKEN;
      sbIn.pvBuffer = pIn;
   }
   ss = InitializeSecurityContext(&pAS->hcred, 
         pAS->fInitialized ? &pAS->hctxt : NULL, NULL, 0, 0, 
         SECURITY_NATIVE_DREP, pAS->fInitialized ? &sbdIn : NULL,
         0, &pAS->hctxt, &sbdOut, &fContextAttr, &tsExpiry);
   if (ss < 0)  { 
      // 
      fprintf(stderr, "InitializeSecurityContext failed with %08X\n", ss);
      return FALSE;
   }
   pAS->fHaveCtxtHandle = TRUE;
   // If necessary, complete token
   if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) {
     ss = CompleteAuthToken(&pAS->hctxt, &sbdOut);
     if (ss < 0)  {
        fprintf(stderr, "CompleteAuthToken failed with %08X\n", ss);
        return FALSE;
     }
   }
   *pcbOut = sbOut.cbBuffer;
   if (!pAS->fInitialized)
      pAS->fInitialized = TRUE;
   *pfDone = !(ss == SEC_I_CONTINUE_NEEDED 
         || ss == SEC_I_COMPLETE_AND_CONTINUE );
 DEBUGMSG(TRUE, (_T("-GenServerContext\r\n")));
 return TRUE;
}

/////////////////////////////////////////////////////////////////////////////// 

BOOL GenServerContext(PAUTH_SEQ pAS, PVOID pIn, DWORD cbIn, PVOID pOut,
      PDWORD pcbOut, PBOOL pfDone) 
{
 DEBUGMSG(TRUE, (_T("+GenServerContext\r\n")));
 SECURITY_STATUS ss;
 TimeStamp       tsExpiry;
 SecBufferDesc   sbdOut;
 SecBuffer       sbOut;
 SecBufferDesc   sbdIn;
 SecBuffer       sbIn;
 ULONG           fContextAttr;
 if (!pAS->fInitialized)
 {
  DEBUGMSG(TRUE, (_T("Calling AcquireCredentialsHandle...")));
  ss = AcquireCredentialsHandle(NULL, SEC_PACKAGE, 
   SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &pAS->hcred, 
   &tsExpiry);
  
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  pAS->fHaveCredHandle = TRUE;
 }
 // Prepare output buffer
 sbdOut.ulVersion = 0;
 sbdOut.cBuffers = 1;
 sbdOut.pBuffers = &sbOut;
 sbOut.cbBuffer = *pcbOut;
 sbOut.BufferType = SECBUFFER_TOKEN;
 sbOut.pvBuffer = pOut;
 // Prepare input buffer
 sbdIn.ulVersion = 0;
 sbdIn.cBuffers = 1;
 sbdIn.pBuffers = &sbIn;
 sbIn.cbBuffer = cbIn;
 sbIn.BufferType = SECBUFFER_TOKEN;
 sbIn.pvBuffer = pIn;
 DEBUGMSG(TRUE, (_T("Calling AcceptSecurityContext...")));
 ss = AcceptSecurityContext(&pAS->hcred, 
   pAS->fInitialized ? &pAS->hctxt : NULL, &sbdIn, 0, 
   SECURITY_NATIVE_DREP, &pAS->hctxt, &sbdOut, &fContextAttr, 
   &tsExpiry);
 if (ss < 0) 
 {
  DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
  return FALSE;
 }
 DEBUGMSG(TRUE, (_T("ok\r\n")));
 pAS->fHaveCtxtHandle = TRUE;
 // If necessary, complete token
 if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) 
 {
  DEBUGMSG(TRUE, (_T("Calling CompleteAuthToken...")));
  ss = CompleteAuthToken(&pAS->hctxt, &sbdOut);
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
 }
 *pcbOut = sbOut.cbBuffer;
 if (!pAS->fInitialized)
  pAS->fInitialized = TRUE;
 *pfDone = !(ss = SEC_I_CONTINUE_NEEDED 
   || ss == SEC_I_COMPLETE_AND_CONTINUE);
 DEBUGMSG(TRUE, (_T("-GenServerContext\r\n")));
 return TRUE;
}

/////////////////////////////////////////////////////////////////////////////// 

BOOL WINAPI SSPLogonUser(LPTSTR szDomain, LPTSTR szUser, LPTSTR szPassword) 
{
 DEBUGMSG(TRUE, (_T("\r\n+SSPLogonUser\r\n")));
 AUTH_SEQ    asServer   = {0};
 AUTH_SEQ    asClient   = {0};
 BOOL        fDone      = FALSE;
 BOOL        fResult    = FALSE;
 DWORD       cbOut      = 0;
 DWORD       cbIn       = 0;
 DWORD       cbMaxToken = 0;
 PVOID       pClientBuf = NULL;
 PVOID       pServerBuf = NULL;
 SecPkgInfo *pSPI       = NULL;
 HMODULE     hModule    = NULL;
 SECURITY_STATUS ss;
 SEC_WINNT_AUTH_IDENTITY ai;
 ULONG packages = 0;
 PSecPkgInfo pPackageInfo = NULL;
 __try 
 {
  EnumerateSecurityPackages(&packages, &pPackageInfo);
  DEBUGMSG(TRUE, (_T("  Available security packages:\r\n")));
  for(UINT i = 0 ; i < packages ; i++)
  {
   DEBUGMSG(TRUE, (_T("\t%s\r\n"), pPackageInfo[i].Name));
  }
  DEBUGMSG(TRUE, (_T("\r\n")));

  DEBUGMSG(TRUE, (_T("Calling FreeContextBuffer...")));
  ss =FreeContextBuffer(pPackageInfo);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Get max token size
  DEBUGMSG(TRUE, (_T("Calling QuerySecurityPackageInfo...")));
  ss = QuerySecurityPackageInfo(SEC_PACKAGE, &pSPI);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  cbMaxToken = pSPI->cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling FreeContextBuffer...")));
  ss =FreeContextBuffer(pSPI);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Allocate buffers for client and server messages
  DEBUGMSG(TRUE, (_T("Allocating heaps...")));
  pClientBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
  pServerBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Initialize auth identity structure
  ZeroMemory(&ai, sizeof(ai));
  ai.Domain = szDomain;
  ai.DomainLength = _tcslen(szDomain);
  ai.User = szUser;
  ai.UserLength = _tcslen(szUser);
  ai.Password = szPassword;
  ai.PasswordLength = _tcslen(szPassword);
  ai.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
  // Prepare client message (negotiate) .
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenClientContext\r\n")));
  if (!GenClientContext(&asClient, &ai, NULL, 0, pClientBuf, &cbOut, &fDone))
   __leave;
  // Prepare server message (challenge) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenServerContext\r\n")));
  if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, &fDone))
   __leave;
  // Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED
   // in the case of bad szUser or szPassword.
   // Unexpected Result: Logon will succeed if you pass in a bad szUser and 
   // the guest account is enabled in the specified domain.
  // Prepare client message (authenticate) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenClientContext\r\n")));
  if (!GenClientContext(&asClient, &ai, pServerBuf, cbIn, pClientBuf, &cbOut, &fDone))
   __leave;
  // Prepare server message (authentication) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenServerContext\r\n")));
  if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, &fDone))
   __leave;
  fResult = TRUE;
 } 
 __finally 
 {
  // Clean up resources
  if (asClient.fHaveCtxtHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling DeleteSecurityContext\r\n")));
   DeleteSecurityContext(&asClient.hctxt);
  }
  if (asClient.fHaveCredHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling FreeCredentialsHandle\r\n")));
   FreeCredentialsHandle(&asClient.hcred);
  }
  if (asServer.fHaveCtxtHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling DeleteSecurityContext\r\n")));
   DeleteSecurityContext(&asServer.hctxt);
  }
  if (asServer.fHaveCredHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling FreeCredentialsHandle\r\n")));
   FreeCredentialsHandle(&asServer.hcred);
  }
  DEBUGMSG(TRUE, (_T("Freeing heaps...")));
  HeapFree(GetProcessHeap(), 0, pClientBuf);
  HeapFree(GetProcessHeap(), 0, pServerBuf);
  DEBUGMSG(TRUE, (_T("ok\r\n")));
 }
 DEBUGMSG(TRUE, (_T("-SSPLogonUser\r\n")));
 return fResult;
}
2/8/2005 1:29:26 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  |