Monday, December 26, 2005

While some people were out shopping for bargains, or returning gifts -- today is the day after Christmas, and that's what many people do -- I spent all day (literally) trying to work out a way to merge managed CLR .NET Forms code with the layout engine, which is unmanaged C++ code. For an introduction to .NET, the CLR, and managed code, check out the following link at wikipedia.org.

C++, as I understand it, is the only programming language designed by Microsoft to allow programmers to write code which combines unmanaged source with managed sources. Clearly, Microsoft was aware, given the enormous amount of code written in C++, that .NET (or at least, the CLR) would die a rapid death if some way to combine managed and unmanaged code did not exist (Apple did a similar thing I suppose by supporting Carbon, to allow legacy Mac code to port to what was otherwise a Cocoa-based world). C# and Visual Basic are perhaps the most commonly used languages supported by the CLR, other than C++, but neither allows programmers to mix both managed and unmanaged sources together.

There were a few thorny issues that I ran into while trying to get the layout code up and running under .NET. Perhaps the most difficult of these was a known bug in Visual Studio .NET 2003 that I ran into that, luckily for me, some kind soul at Microsoft had documented well enough for me to work around (see http://support.microsoft.com/?id=814472 if you care to read about the details, and if you are developing DLLs that mix managed and unmanaged code, as I am, you really must take a look).

Here is where I stand now with things after maybe 15 hours of hacking away on Windows:

-- both jsengine and layout are completely ported, and the job (except for wrangling with the toolchain) was trivial. I can parse (using the same expat-based code) a XUL document, and I call and execute JS from it. The DOM works nicely, and my DOM dump tool shows the structure for a complicated XUL document just as it should be shown.

-- I have coded stub routines for all of the concrete Windows .NET Form classes.

-- in one of the stubs, the concrete WindowImpl class, I instantiate a managed class that I am able to embed in the non-managed wrapper -- again, thanks to some code I found via google. Let's look at this in more detail, because it is important to understand how my non-CLR code can create and interact with CLR-based managed code, a requirement if I am going to make use of .NET Forms. Below is the code, first the header, and followed by an explanation:

#if !defined(__WINDOWSWINDOWIMPL__H__)
#define __WINDOWSWINDOWIMPL__H__

#include "../windowimpl.h"

#include "stdafx.h"

// for discussion of gcroot.h, see
// http://www.ondotnet.com/pub/a/dotnet/2003/03/03/mcppp2.html?page=2

#include <gcroot.h>

#using <mscorlib.dll>
#using <System.dll>
#using <System.Drawing.dll>
#using <System.Windows.Forms.dll>

using namespace System;
using namespace System::ComponentModel;
using namespace System::Drawing;
using namespace System::Windows::Forms;

#include "windowswidgetimpl.h"

__gc class FormImpl : public Form
{
public:
FormImpl() {};
};

class WindowsWindowImpl : public WindowImpl, public WindowsWidgetImpl
{
public:
WindowsWindowImpl();
virtual ~WindowsWindowImpl();
virtual PRStatus Create();
virtual PRStatus Show();
virtual PRStatus Hide();
virtual PRStatus SetTitle(const string& title);
virtual PRStatus HandleCommand() {return PR_SUCCESS;};
virtual PRStatus SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask) {
return SetGeometryImpl(x, y, width, height, mask); };
virtual PRStatus GetGeometryRequest(int &x, int &y,
int &width, int &height) {
return GetGeometryRequestImpl(x, y, width, height); };
private:

// see comment on gcroot, above

gcroot<FormImpl *> m_form;
};
#endif

In the above, the following lines:

#include "stdafx.h"

// for discussion of gcroot.h, see
// http://www.ondotnet.com/pub/a/dotnet/2003/03/03/mcppp2.html?page=2

#include <gcroot.h>

#using <mscorlib.dll>
#using <System.dll>
#using <System.Drawing.dll>
#using <System.Windows.Forms.dll>

using namespace System;
using namespace System::ComponentModel;
using namespace System::Drawing;
using namespace System::Windows::Forms;

bring in (what I believe are) required headers and such for using .NET forms code. The source, compiled with /clr, will fail to compile unless this code is included. The gcroot include itself is actually optional for pure managed code, but if you want to embed a managed class pointer in a non-managed class, you will need to include it, and you must declare the member variable holding the pointer to the managed class like this:

private:

// see comment on gcroot, above

gcroot<FormImpl *> m_form;

This template class gcroot does some magic that I have not yet been forced to understand. But without it, the compiler will complain loudly that you cannot embed a managed object in an unmanaged container. I guess the only other way out, had this solution not existed (and I thought about doing this before I stumbed upon the gcroot paradigm), would have been to maintain a map of managed FormImpl classes and WindowImpl classes, and do lookups as needed to map one to the other in the layout engine. Thankfully, I was able to avoid going down that path completely. I suspect I will need to use the same solution to embed, for example, a managed .NET Form Button object inside of my unmanaged ButtonImpl class. Should be straightforward enough.

And now the source for the WindowImpl Create() function, which shows how I instantiate an instance of FormImpl and stuff it into the m_form member variable:

PRStatus WindowsWindowImpl::Create()
{
m_form = __gc new FormImpl();
if (m_form)
return PR_SUCCESS;
return PR_FAILURE;
}

That was simple enough. To set the title of the Window, I merely need to set the Form's text attribute using code that is very familiar I am sure to .NET Forms programmers:

PRStatus WindowsWindowImpl::SetTitle(const string& title)
{
if (m_form)
m_form->Text = title.c_str();
return PR_SUCCESS;
}

So, things are looking great. Not as fast as going to Gtk+ from Cocoa, but not bad for a couple of days of work. The secret in the speed comes from designing abstractions that were able to accomodate the requirements of each platform toolkit (and a little luck). If all goes well, I should have the rest of the concrete .NET classes coded up shortly and be able to post my first Windows screenshot.

One final thing. Porting to Visual C++ identified some rather subtle, but horrible errors in my code (some uninitialized member variables, for example) that led to crashes that I had yet to see from gcc(1)-generated code. I'm not the first one to say this, but it warrants repeating -- the more compilers you toss your code too, the more likely your code isn't going to blow up in your users faces, because the more likely it will be that errors will be caught. On Windows, that other compiler could be Borland, gcc (via cygwin), CodeWarrior, or Visual Studio, and I am sure several others exist if you look hard enough. On Unix, there are fewer choices, but you can certainly take the cross platform code over to MacOS X and Windows and give it a workout there. The more cross-platform your code is, the more able you will be to take advantage of this tactic for identifying weaknesses in your code.

Just do it.