Tuesday, October 31, 2006

Some info that's worth reading:

10/31/2006 12:45:58 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 

NETCFRPM is a great tool, but it's kind of obvious that it was a late addition to the SP1 SDK.  One big problem is the process of deploying.  There are a couple files that must be pushed to the device in order to run, and these are documented only in Steven Pratchner's blog (maybe elsewhere too, but they aren't in the RPM readme deployed with the SDK). Don't get me wrong, I'd rather have it as an afterthought than not at all.

At any rate, without these files you'll launch RPM, but a device connected via ActiveSync won't show up in the Device ComboBox. Once deployed, RPM pushes uses RAPI to do it's work.  What surprises me is that RPM uses RAPI for data transfer, so why doesn't it use RAPI at the start to find the device and push these files to begin with?  The likely answer is that the team just didn't have time to get it done.  Well, since my general philosophy is any time that I do the same operation more than three times, it needs to be automated.  This is especially true when I have to go hunting for files, (or even the list of files that I can't always recall).

So I created a simple bootstrap that does all this for you.  It requires the OpenNETCF.Desktop.Communication library, but that's about it.  It could be extended to correctly detect target processor architecture (currently just uses ARM), and even do the RAPI provisioning (see Steven Blog entry) for WM 5.0 devices that need it, but at the current moment it works with my devices, so it's unlikely that I'll make changes unless I start testing another device.

At any rate, here's the first cut (download the binary here):

namespace OpenNETCF.RPMBootstrap
{
  class Program
  {
    static void Main(string[] args)
    {
      RapiBootstrap();
    }

    private static OpenNETCF.Desktop.Communication.RAPI m_rapi;

    private static string[] m_filnames = new string[] 
   
      @"netcfrtl.dll",
      @"netcflaunch.exe"
    };

    private const string SDK_REG_KEY = @"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.0.0\InstallRoot";
    private const string SOURCE_PATH = @"\WindowsCE\wce400\armv4";
    private const string TARGET_PATH = @"\Windows";
    private const string CE4_ARM_PATH = @"WindowsCE\wce400\armv4";
    private const string RPM_PATH = @"bin\NetCFRPM.exe";

    public static void RapiBootstrap()
    {
      System.Console.WriteLine("OpenNETCF RPMBootstrap Bootstrap");
      System.Console.WriteLine("================================");

      m_rapi = new OpenNETCF.Desktop.Communication.RAPI();

      System.Console.WriteLine("Connecting via ActiveSync...");
      m_rapi.Connect(true, 10000);

      if (!m_rapi.Connected)
      {
        System.Console.WriteLine("Connection FAILED");
        return;
      }
      System.Console.WriteLine("Connected");

      // determine SDK install folder
      Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(SDK_REG_KEY);
      string sdkPath = (string)key.GetValue(null);
      key.Close();
      System.Console.WriteLine("CF 2.0 SDK is installed at:\n\t" + sdkPath);
      string localFilePath = System.IO.Path.Combine(sdkPath, CE4_ARM_PATH);

      foreach (string fileName in m_filnames)
      {
        string targetFile = System.IO.Path.Combine(TARGET_PATH, fileName);

        if (!m_rapi.DeviceFileExists(targetFile))
        {
            System.Console.WriteLine(string.Format("{0} not on device", fileName));
            System.Console.Write("Copying...");
            string localFile = System.IO.Path.Combine(localFilePath, fileName);
            m_rapi.CopyFileToDevice(localFile, targetFile);
            System.Console.WriteLine("ok");
        }
        else
        {
            System.Console.WriteLine(string.Format("{0} already on device", fileName));
        }
      }
      System.Console.Write("Disconnecting...");
      m_rapi.Disconnect();
      System.Console.WriteLine("ok");

      // find RPM
      string rpmPath = System.IO.Path.Combine(sdkPath, RPM_PATH);

      if (!System.IO.File.Exists(rpmPath))
      {
        System.Console.WriteLine("Local RPM Binary not found at:\r\t" + rpmPath);
        return;
      }

      // launch
      System.Console.WriteLine("Launching RPM...");
      System.Diagnostics.Process p = System.Diagnostics.Process.Start(rpmPath);
    }
  }
}

10/31/2006 12:35:18 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Thursday, October 26, 2006

If you know much about me, you probably know that simple doesn't excite me.  I like problems that are interesting yet tough to solve.  A year or more ago or so I decided that I really needed to know how CAB files work.  If you do much work creating CE devices you're familiar with the frustration of trying to include some third party package in your device image, but all you have is a CAB.  So you have to find all of the files, folders registry entries and the like that the CAB creates - usually through a process of elimination.

Well I knew that a CAB file has to contain that info, and experience had told me that just unpacking the CAB gives you all of the files (though with mangled names).  So I started playing in my free time reverse engineering how they're packed up.  Like many of my side projects, I'd tabled the project a couple months ago due to a lack of time and paying work that needed doing.  That was until this week, when I needed to combine a third-party CAB with a customer's app to give them a single-CAB deployment.  I spent a couple days working on the tool to get it to generate an INF file that was compatible with CABWIZ.  And that brings us to the quiz.  The following is an output from the tool - right from the source 3rd party CAB.  If I run this and the files into CABWIZ I get the came CAB back out that installs on the device as expected.  Any guess on what the CAB is (yes it's so easy it's a rhetorical question)?

; +-------------------------------------------------+
; | INF Generated by OpenNETCF CABConstructor       |
; | visit
http://www.OpenNETCF.com for product info |
; +-------------------------------------------------+

[Version]
Signature="$Windows NT$"
CESignature="$Windows CE$"
Provider="Microsoft"

[CEStrings]
AppName=".NET CF 2.0"
InstallDir="%CE2%"

[DefaultInstall]
AddReg="RegKeys"
CESetupDLL="NETCF_~1.dll"
CopyFiles="Destination3","Destination2"

[CEDevice]
VersionMin="4.0"
VersionMax="4.999"
BuildMax="0xE0000000"
UnsupportedPlatforms="HPC","JUPITER","SMARTPHONE"

[SourceDisksNames]
1=,"SRCFILES",,SourceFiles

[SourceDisksFiles]
"00System.008"=1
"0mscoree.006"=1
"0NETCFv2.000"=1
"cgacutil.005"=1
"CGACUT~1.021"=1
"CUSTOM~1.020"=1
"MICROS~1.018"=1
"MICROS~2.017"=1
"MICROS~4.019"=1
"MSCORE~1.002"=1
"MSCORE~1.022"=1
"mscorlib.007"=1
"NETCF2~1.001"=1
"NETCFA~1.003"=1
"NETCFD~1.004"=1
"NETCF_~1.dll"=1
"NETCF_~1.dll"=1
"SY40C7~1.014"=1
"SY4317~1.016"=1
"SY726F~1.010"=1
"SY9B57~1.013"=1
"SYC6B2~1.011"=1
"SYSTEM~1.015"=1
"SYSTEM~3.009"=1
"SYSTEM~4.012"=1
"_setup.xml"=1

[DestinationDirs]
Destination3=0,"\%CE2%\.NET CF 2.0"
Destination2=0,"\%CE2%"

[Destination3]
"mscorlib.dll","mscorlib.007",,0x40000001
"System.dll","00System.008",,0x40000001
"System.Drawing.dll","SYSTEM~3.009",,0x40000001
"System.Messaging.dll","SY726F~1.010",,0x40000001
"System.Web.Services.dll","SYC6B2~1.011",,0x40000001
"System.Windows.Forms.dll","SYSTEM~4.012",,0x40000001
"System.Windows.Forms.DataGrid.dll","SY9B57~1.013",,0x40000001
"System.Xml.dll","SY40C7~1.014",,0x40000001
"System.Net.IrDA.dll","SYSTEM~1.015",,0x40000001
"System.Data.dll","SY4317~1.016",,0x40000001
"Microsoft.VisualBasic.dll","MICROS~2.017",,0x40000001
"Microsoft.WindowsCE.Forms.dll","MICROS~1.018",,0x40000001
"Microsoft.WindowsMobile.DirectX.dll","MICROS~4.019",,0x40000001
"CustomMarshalers.dll","CUSTOM~1.020",,0x40000001
"cgacutil.exe.-410~-410~ARMV4","CGACUT~1.021",,0x40000001
"mscoree.dll.-410~-410~ARMV4","MSCORE~1.022",,0x40000001

[Destination2]
"netcf2_0license.txt","NETCF2~1.001",,0x40000001
"MSCOREE2_0.dll","MSCORE~1.002",,0x40000001
"netcfagl2_0.dll","NETCFA~1.003",,0x40000001
"netcfd3dm2_0.dll","NETCFD~1.004",,0x40000001
"cgacutil.exe","cgacutil.005",,0xA0000001
"mscoree.dll","0mscoree.006",,0xA0000001

[RegKeys]
HKLM,"GACPath",""%InstallDir%"",0x00000000,"%InstallDir%"

 

10/26/2006 10:33:22 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [4]  | 
 Wednesday, October 25, 2006

So a couple of us at OpenNETCF have been heads down coding in a project for the last couple months.  It's got some interesting aspects and I'll be posting lessons learned over the coming weeks.  One of the biggest problems is that the deployment device for testing is the HP6315, which I'll go on record as saying sucks.  Working around its flakiness is a real pain.  Another problem is the customer's desire to run on the device out-of-the-box to minimize deployment.  That means running with the version of the CF in ROM (CF 1.0 SP2 in this case).

Well the last few bugs in the project's bug database are that the app can't restart once it's been closed.  The first suspect is obviously that the app didn't really shut down all the way, and the CF is preventing new instances from running.

First step is to repro.  It's odd becasue we've been developing for a couple months and neitehr of us have seen this.  We run through the reported repro steps from the customer's functional testing and can't get it to fail.  The customer can repro it every time, and on multiple devices.  It then dawned on me to try a hard-reset, clean device.  Blam!  Sure enough, it failed.

So what's different?  Well in development we use Studio, which pushes down the latest CF (SP3) the first time and then we forget about it.  SP3 fixed something in SP2.  Of course the requirement is to use what's in ROM, so it's time to debug a little.

Next step is to use Remote Process Viewer and confirm that the process is indeed still running before digging into the code.

Well RPC shows that the second instance did in fact launch - now we have 2 instances.  Further, tapping the icon starts more instances.  If you don't understand the implications of this, let me explain a little.  The CF (on PPC devices) enforces singleton app behavior (a bad idea IMHO, but that's a side topic).  Well if we're getting multiple instances, then this "enforcement" is failing  The CF is not only not shutting the first down, but the second is being prevented from fully coming up (maybe the assembly is locked in some way?).

Well, added a little debug code and we find that Application.Run is exiting and Main is running out it's course.  Again, this points at a zombie thread, but that doesn't explain the multiple instance issue.

I spent a couple hours walking our code and a 3rd party library, trying to find a zombie thread or anything that might be doing this to no avail, and then it dawned on me - the app is closing.  We don't need any resources, we just want to end.  Well, let's just be heavy handed

I looked up ExitProcess in the platform headers, and it's and inline that calls TerminateProcess with GetCurrentProcess for the handle.  Digging further leads you to find that GetCurrentProcess() is really just the constant 0x42 (on ARM anyway).  So this is what I ended up with (well trimmed down anyway):

static void Main()
{
    Main main = new Main();
    Application.Run(main);

    if((System.Environment.Version.Major == 1) && (System.Environment.Version.Build < 4292))
    {
        // NOTE:
        // This is a heavy-handed workaround for a bug this app exposes in pre-SP3 CF 1.0 devices
        // For those interested, 0x42 is the expansion of GetCurrentProcess() from the platform SDK headers
        // and ExitProcess is simply an inline calling TerminateProcess with GetCurrentProcess(). 
        TerminateProcess(0x42, 0);
    }
}

[DllImport("coredll.dll", SetLastError=true)]
private static extern int TerminateProcess(uint handle, int uExitCode);

Long live the kludge! 

10/25/2006 6:34:11 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Friday, August 25, 2006

If you read much of what I write, you know that performance is something I like to always keep in mind, and I'm also a big fan of quantitative analysis.  It all started ages ago with a look at the real time capabilities of Windows CE, but most recently I've been looking at managed code.  This entry is the second in a series of looks at performance of managed code, specifically the .NET Compact Framework.  For the first entry, see this blog entry (which became this MSDN article).

Act II - Method calls are Expensive (or Stay in the Shallow End of the Stack!)

As with many of my diatribes into technical problems, this all started with a newsgroup post.  SOmeone posted that the had done a Towers of Hanoi implementation in VB.NET, C# and C++ and that the C# implementation was faster than VB, but got slower in CF 2.0.  They also said that native code was much faster than both (I think he may have said an order of magnitude, but don't quote me on that).

Well he didn't post any code so we could reproduce his results, and I never take anyone on their word on something like this, so I decided to put it to the test.  Before going any further, you may need some background information on what the Towers of Hanoi problem is.  You're probably familiar with it - you just didn't know what it was called.  Go read this Wikipedia entry and come back.

I decide to do a recursive solution, so the meat of the problem is that we have an exponential number of method calls, making the call stack very, very deep.  Just from a memory point of view this is a bad idea (see my MEDC presentation on memory management if you want to know more on the whys of that).  But you'll also see that the expense of method calls in the CF (see Act I) really bites you here as it bites hard.

First, let's look at the code.  In C#, it looks like this:

public class Hanoi
{
  private int[] pegs = new int[3];

  public Hanoi(int totalDisks)
  {
    // start with all disks on peg 0
    pegs[0] = totalDisks;
    pegs[1] = 0;
    pegs[2] = 0;
  }

  public int Solve()
  {
    int et = Environment.TickCount;
    Move(0, 2, 1, pegs[0]);
    return Environment.TickCount - et;
  }

  private void Move(int fromPeg, int toPeg, int intermediatePeg, int disks)
  {
    if(disks == 0) return;

    // move all but one disk to the intermediate peg
    Move(fromPeg, intermediatePeg, toPeg, disks - 1);

    // move the last remaining disk to the destination - no need for the intermediate
    pegs[fromPeg] -= 1;
    pegs[toPeg] += 1;

    // now move all but one off the intermediate peg to the destination peg
    Move(intermediatePeg, toPeg, fromPeg, disks - 1);
  }
}

You can see that Move calls itself twice.  Now try tracing the code in your head if I call Solve when totalDisks is 30.  Ugly.

Now lets look at the C++ implementation.

class Hanoi
{
    private:
        int pegs[3];
        void Move(int fromPeg, int toPeg, int intermediatePeg, int disks);

    public:
        Hanoi(int totalDisks);
        int Solve();
};

Hanoi::Hanoi(int totalDisks)
{
    // start with all disks on peg 0
    pegs[0] = totalDisks;
    pegs[1] = 0;
    pegs[2] = 0;
}

void Hanoi::Move(int fromPeg, int toPeg, int intermediatePeg, int disks)
{
    if(disks == 0) return;

    // move all but one disk to the intermediate peg
    Move(fromPeg, intermediatePeg, toPeg, disks - 1);

    // move the last remaining disk to the destination - no need for the intermediate
    pegs[fromPeg] -= 1;
    pegs[toPeg] += 1;

    // now move all but one off the intermediate peg to the destination peg
    Move(intermediatePeg, toPeg, fromPeg, disks - 1);
}

int Hanoi::Solve()
{
    int et = GetTickCount();
    Move(0, 2, 1, pegs[0]);
    return GetTickCount() - et;
}

You can see that it's really not much different - in fact it's really, really close.  So what do we see for results when we run these? Look at the table below (my device was an Axim X30 with a PXA270 processor)

disks C# - CF 2.0 C++ Diff % improvement
20 744 461 283 38%
21 1518 1148 370 24%
22 2997 2068 929 31%
23 5964 4006 1958 33%
24 11892 7646 4246 36%
25 23942 14875 9067 38%
26 47945 29423 18522 39%
27 95674 58496 37178 39%
28 190917 116837 74080 39%

The C++ version performed nearly 40% better.  Of course the C# JIT compiler is running on a mobile device, with presumably much less power than a desktop machine, so the JITter is built to optimize for compile speed, not execution speed.  To try to level the playing field, I compiled the C++ version in Debug mode to turn off all compiler optimizations.  I can't actually see what the CF JITter creates for assembly, so we can't be sure if the resulting code is the same, but that's the nearest I can come.

So we know that the implementation code is near identical.  We also know from Act I that identical code in a single method has no performance difference between native and managed code.  We also know from Act I that method calls are quite expensive.  A reasonable conclusion then is that the performance degradation we're seeing here is nearly all in the cost of method calls.  Does this mean that managed code performance is terrible?  The answer is "it can be if you don't fully understand what the CLR is doing." The lesson learned here then is to either refactor the algorithm to keep your call stack short (there are non-recursive solutions to this problem - maybe another day I'll test that), or put heavily recursive stuff into a native library and P/Invoke to it.

If you want to try these out on your own device, you can download the source code here (post your results in the comments if you'd like).  The source actually points out another lesson, this time in UI development speed. The C# version has a nice UI that I put together in about 10 minutes.  Writing a nice UI would have taken quite a bit longer in C++ so I didn't even bother. With the C++ version you have to get the results from by running in eVC or Studio and setting a breakpoint in the calling loop and writing down the number after each iteration.

8/25/2006 12:09:58 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Wednesday, August 23, 2006

We've had an idea for a new project for some time, and we've finally decided to put out a code seed for it to see if the community wants to get involved.

The idea is this - Create a native coredll.dll library for the desktop that exactly matches the one exposed by Windows CE (same funtions at the same ordinals).  In theory, this would allow you to run Compact Framework applications against the full framework including code that P/Invokes.  The long-term goal is to implement every funtion (there are about 1800 of them, we've seeded the project with 50), but the milestone I'm shooting for is to get this library to a point that the SDF will run on the desktop.

Why would you want this library you ask?  The answer is fairly simple - to help in debugging and unit testing.  I don't envision you shipping a product that runs on CE and XP, but I do see great value in being able to run your CF assemblies through NUnit or Team Suite unit tests, which today cannot be done on a device.  This project is an enabler for that.

THe project is located at CodePlex as the OpenNETCF Advanced Debugging Toolkit.  Look for more pieces to the toolkit as time progresses.

8/23/2006 2:00:11 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1]  |