Mono is an open source version of the .NET framework; it allows you to run .NET applications not just on Windows but on Linux and the Mac. I’ve spent quite some time over the last week getting our Python spreadsheet, Resolver One, to run on the Windows version, and thought it would be worth sharing some experiences.
Some background first: one of our long-term goals at Resolver Systems is to get our currently Windows-only products working on other platforms. Everything’s coded in .NET, so in an ideal world we’d just be able to run it all under Mono. However, there are two problems:
- Some of the third-party components we use are “.NET” in the sense that they offer us a .NET interface, but under the hood they call down to lower-level Windows functions using P/Invoke. Because they’re using Windows-specific stuff, they won’t run on non-Windows operating systems, even with Mono.
- As always with these things, while there is a formal specification for what .NET implementations like Microsoft’s .NET or Mono are meant to do (called the CLI), implementations differ due to bugs, things that are awaiting implementation, or ambiguities in the spec.
Obviously, we need to handle both of these kinds of problem to successfully port our software. We’re handling the first kind by moving over to newer, “pure” .NET components, for example by writing our own grid. But we didn’t want to finish all that work and only then discover all of the problems caused by the second kind of incompatibility. Now, Mono does support P/Invoke, so while the first kind of problem clearly prevents us from running on Mono right now on, say, a Mac, it does not prevent us from running on Mono on Windows. So we decided to do that simpler port in parallel with our development of the new components, so that we could find out any nasty issues of the second kind as early as possible.
First things first: it really was surprisingly easy! All kudos to the Mono developers, this really is an example of an open source project that works really well. The problems below are really low-level details, and most of them are unlikely to hit anyone apart from us. However, it’s worth enumerating them, at least for posterity’s sake — and perhaps they’ll be helpful for people Googling for solutions to similar obscure problems.
Problem 1: A change to the process binary
The first problem we hit was our code to load up our DLLs. Resolver One is comprised of quite a few libraries, and we need to be careful to load the specific ones that it’s shipped with rather than others that might be on the user’s path. We do this by finding our binary’s location, using
Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.Filename), and then using
Assembly.LoadFile(filename) to load the DLLs explicitly rather than the more normal
clr.AddReference, which uses the path.
The problem is, when you run a .NET application under Mono, the current process’s binary is not (say)
Resolver-One.exe, but instead
mono.exe. So Resolver One was trying to find its libraries in the Mono install directory, which obviously didn’t work. In the short term, we were able to work around this by hard-wiring the DLL path. In the longer term, we’ll have to do something more clever…
Problem 2: ImeModeBase
Once we’d fixed the first problem we got a new error:
Could not find "System.Windows.Forms.Control.get_ImeModeBase". A bit of investigation made it clear that the current version of Mono doesn’t support this method (though when it does, the target of that link will show it). It looks like the method was introduced in the (relatively recent) .NET 2.0 SP2, and presumably it will be implemented sometime, but it’s not there right now.
The question was, could Resolver One run without this method? The answer seemed likely to be “yes”, as we were able to run on earlier versions of .NET 2.0. We took a look at our codebase and tried to find out what it was that was referencing the method. It turned out to be our “precompiled subclass” DLLs. These are something we introduced a while back to improve our startup performance; simplifying a bit, what happens is that when we package up Resolver One for distribution, we run a compile process on all of the classes in our code that are subclasses of .NET components. Doing this process once before we release the software means that it’s not done every time Resolver One starts up, with obvious performance benefits. The downside is that the compiled subclasses have explicit references to the members of their .NET superclasses, whether they use them or not. And because we run the compilation process under .NET 2.0 SP2, our compiled subclasses wind up with explicit references to stuff that doesn’t exist in earlier versions of .NET, or (as it turns out) in Mono.
The good news is that if you’re willing to take a performance hit, the subclass compilation can be made optional. This isn’t something we put in the production version of Resolver One right now, as it’s Windows only (and so there’s little point in having a “start up more slowly for no useful reason” command-line option), but it was easy to add in. Using non-precompiled subclasses got us past this problem. Perhaps our future Linux/Mac-compatible version can use subclasses that are precompiled against Mono.
Problem 3: System.Reflection:MonoGenericMethod
This one was the easiest one to find out how to work around, but took the longest time. Once we’d got past the precompiled assembly problem, trying to start Resolver One gave us a dialog saying “** ERROR **: Can’t handle methods of type System.Reflection:MonoGenericMethod aborting…”. Mono then bombed out with a Windows “application has stopped working” dialog.
A bit of Googling followed, and we were delighted to discover that a bug that triggered this exact error message had been fixed just ten days previously on the Mono trunk and the 2.6 branch. There’s luck for you.
Unfortunately we also discovered that the Mono team don’t release nightly builds of their Windows installer, so the only ways to get this fix would be either to wait until the 2.6.5 release, or to build it ourselves. Being impatient by nature, we decided to do the latter, and this took quite a while. I’ll post separately about what we had to do, as it may be useful for others; there’s a lot of excellent documentation on building Mono for Windows, but some of it’s a bit out of date. Luckily, the people on the Mono email list are friendly and helpful, so we were able to get there in the end.
So, after a bit of work we had a working version of Mono built from the tip of the 2.6 branch.
Problem 4: Logfile locations
When we started up Resolver One with the new Mono, we got the error
SystemError: Access to the path "C:\ResolverOne.log" is denied. This was interesting, because our default logfile location is determined using
Path.GetTempPath(). I’m not sure where Mono picks up the value for that, but presumably it was returning an empty string. Perhaps we were missing something in our environment variables? Either way, we decided to work around it by using Resolver One’s
--logfile command-line option.
Problem 5: Madness in our methods
When told to log to an appropriate directory, Resolver One started up, and things started looking pretty good! The splash screen appeared, the “starting” progress bar moved and then… it crashed. The log file had this in it:
Exception: Method DLRCachedCode:FormulaLanguage.parsetab$16 (IronPython.Runtime.CodeContext,IronPython.Runtime.FunctionCode) is too complex. CLS Exception: System.InvalidProgramException: Method DLRCachedCode:FormulaLanguage.parsetab$16 (IronPython.Runtime.CodeContext,IronPython.Runtime.FunctionCode) is too complex. at IronPython.Compiler.OnDiskScriptCode.Run () [0x00000] in
A bit of Googling lead us to two pages that suggested that “too complex” means that the method in question was too long. The module
FormulaLanguage.parsetab is, as you might expect, related to the code we use to parse the formula language — that is, the language in which you write formulae in cells. This language is specified in our code in BNF with associated handler code, and compiled down into Python by the excellent PLY. The parsetab module is the generated code, and as you might expect it has some pretty unreadable stuff in it; there’s one dictionary literal that is on one 81,000-character line.
The easy fix to work around this problem was to split
parsetab.py up into multiple modules. There were three variables that were being initialised with oversized literal expressions,
_lt_productions. We created a separate module for each, which simply contained the initialisation code for the specific variables:
lt_productions_file.py. Finally, we replaced the code in
parsetab.py that had been moved to the new files with appropriate import statements: for example, from
lt_action_items_file import _lt_action_items.
This fixed the problem, and allowed us to move onto the next one! It’s not obvious how we’ll fix this in the production release, though — the file is auto-generated, so either we’ll have to patch PLY or post-process it. Something for us to think about.
Problem 6: Extra
The error we got after fixing the parsetab problem was a bit obscure:
Exception: Finalize CLS Exception: System.Collections.Generic.KeyNotFoundException: Finalize at IronPython.Runtime.PythonDictionary.GetItem (object)
The actual location of the error took quite a long time to track down, due to the vagaries of the way we import modules and its effects on stack traces. We eventually wound up having to do a binary chop with log statements in our startup code until we managed to narrow it down to a single line!
It turned out that we have some code that needed to create wrapper functions around all of the functions in a different module. It did this by looping over the values in the dictionary returned by
vars(other_module). However, it didn’t want to wrap functions that were internal to .NET, so it had a list of function names to exclude; specifically,
Equals. Clearly these were two function names that had been found by experiment to belong to IronPython modules when running under .NET. The error we were getting was ultimately being caused by IronPython under Mono having just one extra function visible on the module:
Finalize. Adding that to the list of exclusions got us past this error, and on to:
Problem 7: Um… that’s it!
…on to nothing else! Once we’d fixed the Finalize problem Resolver One started up and ran reasonably happily under Mono on Windows. There were glitches, of course; our code editor component, in particular, didn’t like being resized. But the software worked well enough to test, which is all we need for now.
There’s obviously a lot to be done before we can get Resolver One running on Macs and Linux machines; the creation of our grid component is going well, but takes time, and we need to do something about the code editor. But the good news is that we’ve identified the incompatibilities between Mono and Microsoft .NET that will hit us beyond the obvious operating system issues, and there’s nothing we can’t work around, given a bit of ingenuity. It took a while, but at the end of the day, it was surprisingly easy :-)