Dynamically Compiling C#

Compiling C# from IronPython

Your brain on C#...

 

 

Introduction

IronPython is a great way to use the .NET framework. It comes packed full of Python dynamic goodness. Unfortunately it isn't perfect. One noteworthy hole in the IronPython .NET integration is attributes. You can't use attributes in IronPython, which can sometime be a problem.

The normal way round this problem is to create stub C# classes with methods that you can override in IronPython. This doesn't always work though; sometimes you want to dynamically specify the arguments to the attributes - which can only be done at compile time with C#.

This article explores a way round the problem, with a solution that potentially has many other uses. It provides a way to dynamically compile C# source code into assemblies. These assemblies can be used in memory or saved to disk.

Compiling C#

This code uses the System.CodeDom.Compiler API, along with Microsoft.CSharp.CSharpCodeProvider:

import clr

from System.Environment import CurrentDirectory
from System.IO import Path, Directory

from System.CodeDom import Compiler
from Microsoft.CSharp import CSharpCodeProvider


def Generate(code, name, references=None, outputDirectory=None, inMemory=False):
    CompilerParams = Compiler.CompilerParameters()

    if outputDirectory is None:
        outputDirectory = Directory.GetCurrentDirectory()
    if not inMemory:
        CompilerParams.OutputAssembly = Path.Combine(outputDirectory, name + ".dll")
        CompilerParams.GenerateInMemory = False
    else:
        CompilerParams.GenerateInMemory = True

    CompilerParams.TreatWarningsAsErrors = False
    CompilerParams.GenerateExecutable = False
    CompilerParams.CompilerOptions = "/optimize"

    for reference in references or []:
        CompilerParams.ReferencedAssemblies.Add(reference)

    provider = CSharpCodeProvider()
    compile = provider.CompileAssemblyFromSource(CompilerParams, code)

    if compile.Errors.HasErrors:
        raise Exception("Compile error: %r" % list(compile.Errors.List))

    if inMemory:
        return compile.CompiledAssembly
    return compile.PathToAssembly

It exposes a single function called Generate. The arguments to the Generate function are as follows:

  • code

    The C# source code as a string.

  • name

    The name for the generated assembly (not including '.dll' which will be added automatically if you are saving assemblies to disk).

  • references (optional)

    If your code uses any assemblies outside the standard .NET framework then you will need to provide a list of references. These should be absolute paths to the referenced assemblies.

  • outputDirectory (optional)

    If you want to save the assemblies to disk then provide an ouput directory as a string. If inMemory is False (the default) and you don't supply a directory then the current directory will be used.

  • inMemory (optional - defaults to False)

    Set to True if you want assemblies generating in memory rather than saving to disk.

    If this argument is False then Generate returns the path to the saved assembly. If True, Generate returns a reference to the assembly in memory.

Using Generate

Using Generate is very simple. I'll use as an example the Screenshot Code from my Windows Forms tutorial. This code uses the DllImport attribute to access unmanaged code from GDI32.dll and user32.dll.

The following code shows the C# embedded in IronPython as a string. This is then compiled to an in memory assembly and imported into IronPython. The static methods are then called in exactly the same way as if the assemblies had been referenced from disk:

import clr
clr.AddReference('System.Drawing')

from System.Drawing import Bitmap, Image

from generate import Generate, LoadAssembly


unmanaged_code = """
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace UnmanagedCode
{
    public class GDI32
    {
        [DllImport("GDI32.dll")]
        public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        [DllImport("GDI32.dll")]
        public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth,
            int nHeight);

        [DllImport("GDI32.dll")]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        [DllImport("GDI32.dll")]
        public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest,
                                         int nWidth, int nHeight, IntPtr hdcSrc,
                                         int nXSrc, int nYSrc, int dwRop);

        [DllImport("GDI32.dll")]
        public static extern bool DeleteDC(IntPtr hdc);

        [DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);
    }

    public class User32
    {
        [DllImport("user32.dll")]
        public static extern IntPtr GetDesktopWindow();

        [DllImport("user32.dll")]
        public static extern IntPtr GetTopWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetWindow(IntPtr hWnd, uint wCmd);

        [DllImport("User32.dll")]
        public static extern IntPtr GetWindowDC(IntPtr hWnd);

        [DllImport("User32.dll")]
        public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);

    }
}
"""


assembly = Generate(unmanaged_code, 'UnmanagedCode', inMemory=True)

clr.AddReference(assembly)

from UnmanagedCode import User32, GDI32

def ScreenCapture(x, y, width, height):
    hdcSrc = User32.GetWindowDC(User32.GetDesktopWindow())
    hdcDest = GDI32.CreateCompatibleDC(hdcSrc)
    hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height)
    GDI32.SelectObject(hdcDest, hBitmap)

    # 0x00CC0020 is the magic number for a copy raster operation
    GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, 0x00CC0020)
    result = Bitmap(Image.FromHbitmap(hBitmap))
    User32.ReleaseDC(User32.GetDesktopWindow(), hdcSrc)
    GDI32.DeleteDC(hdcDest)
    GDI32.DeleteObject(hBitmap)
    return result

image = ScreenCapture(0, 0, 50, 400)
for y in range(image.Height):
    row = []
    for x in range(image.Width):
        color = image.GetPixel(x, y)
        value = color.R + color.G + color.B
        if value > 384:
            row.append(' ')
        else:
            row.append('X')
    print ''.join(row)

Note that this code calls Generate to create the assembly, and then adds a reference to the assembly using clr.AddReference. The important lines are:

assembly = Generate(unmanaged_code, 'UnamangedCode', inMemory=True)

clr.AddReference(assembly)

from UnmanagedCode import User32, GDI32

Having added a reference to the assembly, you can then import directly from the 'UnmanagedCode' namespace as usual. In practical use the C# source code could also be dynamically generated, making all sorts of things possible.

If you run this script from the command line then, depending on what you have in the top left of your screen, you should see something a bit like:

Screen to text - very useful...

Subclassing in IronPython

This is all very well, but it still requires writing classes and methods that need to be marked with attributes in C#. What if you really want to write your code in IronPython?

Fortunately that is still easy - you can create a stub class that can be subclassed in IronPython.

The following example C# creates a class with a method both marked with 'SomeAttribute'. 'method' takes two integers and returns a string. We make the method marked with an attribute call another method that is virtual, meaning that it can be overridden from IronPython.

using something;

namespace withattributes
{

    [SomeAttribute]
    class public Class
    {
        [SomeAttribute]
        public string method(int arg1, int arg2)
        {
            return realmethod(arg1, arg2);
        }

        public virtual string realmethod(int arg1, int arg2)
        {
            return "";
        }
    }
}

When you compile this with Generate, you can then import Class from the withattributes namespace:

from withattribtues import Class

class SubClass(Class):

    def realmethod(self, arg1, arg2):
        return str(arg1) + str(arg2)

SubClass still has the attribute on method, but your IronPython code is executed when it is called. Obviously you must observe the types of arguments and return values or .NET gets very unhappy. Smile

Now this isn't the most memory efficient way of doing things. Especially if you are generating a lot of small assemblies - we create an assembly and the type objects every time. Speed is not so much an issue though, this compilation executes much faster than you would expect.

For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store.

Hosted by Webfaction

Return to Top

Page rendered with rest2web the Site Builder

Last edited Fri Nov 27 18:32:35 2009.

Counter...