Metaprogramming with AutoCAD - Part 2

In this post we're going to continue the topic started in Part 1 of this series, which looked briefly at metaprogramming with AutoCAD using AutoLISP and VB(A). Now we're going to look at .NET, focusing initially on C# and VB.NET.

[I found the inspiration for the code in this post from The Code Project, although I had to update the code to use non-deprecated CLR methods as well as making it work for AutoCAD, of course.]

While .NET doesn't provide something as simple as an Eval() function, it actually provides something much more interesting. The CLR exposes the ability to compile and execute source code in .NET languages for which implementations of the CodeDomProvider protocol have been provided.

The Microsoft.CSharp namespace, for instance, contains the CSharpCodeProvider class, which allows you to specify and compile C# code from any .NET language. Microsoft.VisualBasic contains VBCodeProvider, which does the same for VB.NET.

Which means that it's actually very easy to implement dynamic metaprogramming in a homogeneous or heterogeneous fashion from .NET. Yay! 🙂

As I had some time, and decided that implementing this for both C# and VB.NET would tell the story nicely, I've provided code below for both environments.

Here's the C# code, which shows how to compile and execute C# (homogeneous) and VB.NET (heterogeneous) code:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Microsoft.CSharp;

using Microsoft.VisualBasic;

using System.CodeDom.Compiler;

using System.Reflection;

using System.Text;

using System;

namespace Metaprogramming

{

  public class Commands

  {

    const string acadFolder =

      "c:\\Program Files\\Autodesk\\AutoCAD 2008\\";

    // EvalCS: Evaluates C# source

    public static object EvalCS(string csCode)

    {

      DocumentCollection dm =

          Application.DocumentManager;

      Editor ed = dm.MdiActiveDocument.Editor;

      CSharpCodeProvider cs = new CSharpCodeProvider();

      CompilerParameters cp = new CompilerParameters();

      cp.ReferencedAssemblies.Add("system.dll");

      cp.ReferencedAssemblies.Add(acadFolder + "acdbmgd.dll");

      cp.ReferencedAssemblies.Add(acadFolder + "acmgd.dll");

      cp.CompilerOptions = "/t:library";

      cp.GenerateInMemory = true;

      StringBuilder sb = new StringBuilder();

      sb.Append("using System;\n");

      sb.Append("using Autodesk.AutoCAD.Runtime;\n");

      sb.Append(

        "using Autodesk.AutoCAD.ApplicationServices;\n"

      );

      sb.Append("using Autodesk.AutoCAD.DatabaseServices;\n");

      sb.Append("using Autodesk.AutoCAD.EditorInput;\n");

      sb.Append("using Autodesk.AutoCAD.Geometry;\n");

      sb.Append("namespace CSCodeEval{\n");

      sb.Append("public class CSCodeEval{\n");

      sb.Append("public object EvalCode(){\n");

      sb.Append("return " + csCode + ";\n");

      sb.Append("}\n");

      sb.Append("}\n");

      sb.Append("}\n");

      CompilerResults cr =

        cs.CompileAssemblyFromSource(cp, sb.ToString());

      if (cr.Errors.Count > 0)

      {

        ed.WriteMessage(

          "\nErrors evaluating C# code (" +

          cr.Errors.Count +

          "):"

        );

        for (int i = 0; i < cr.Errors.Count; i++)

        {

          ed.WriteMessage(

            "\nLine number " +

            cr.Errors[i].Line + ": " +

            cr.Errors[i].ErrorText

          );

        }

        return null;

      }

      System.Reflection.Assembly a =

        cr.CompiledAssembly;

      object o =

        a.CreateInstance("CSCodeEval.CSCodeEval");

      Type t = o.GetType();

      MethodInfo mi = t.GetMethod("EvalCode");

      object s = mi.Invoke(o, null);

      return s;

    }

    // EvalVB: Evaluates VB source

    public static object EvalVB(string vbCode)

    {

      DocumentCollection dm =

          Application.DocumentManager;

      Editor ed = dm.MdiActiveDocument.Editor;

      VBCodeProvider vb = new VBCodeProvider();

      CompilerParameters cp = new CompilerParameters();

      cp.ReferencedAssemblies.Add("system.dll");

      cp.ReferencedAssemblies.Add(acadFolder + "acdbmgd.dll");

      cp.ReferencedAssemblies.Add(acadFolder + "acmgd.dll");

      cp.CompilerOptions = "/t:library";

      cp.GenerateInMemory = true;

      StringBuilder sb = new StringBuilder();

      sb.Append("Imports System\n");

      sb.Append("Imports Autodesk.AutoCAD.Runtime\n");

      sb.Append(

        "Imports Autodesk.AutoCAD.ApplicationServices\n"

      );

      sb.Append(

        "Imports Autodesk.AutoCAD.DatabaseServices\n"

      );

      sb.Append("Imports Autodesk.AutoCAD.EditorInput\n");

      sb.Append("Imports Autodesk.AutoCAD.Geometry\n");

      sb.Append("Namespace VBCodeEval\n");

      sb.Append("Public Class VBCodeEval\n");

      sb.Append("Public Function EvalCode() As Object\n");

      sb.Append("Return " + vbCode + " \n");

      sb.Append("End Function\n");

      sb.Append("End Class\n");

      sb.Append("End Namespace\n");

      CompilerResults cr =

        vb.CompileAssemblyFromSource(cp, sb.ToString());

      if (cr.Errors.Count > 0)

      {

        ed.WriteMessage(

          "\nErrors evaluating VB code (" +

          cr.Errors.Count +

          "):"

        );

        for (int i = 0; i < cr.Errors.Count; i++)

        {

          ed.WriteMessage(

            "\nLine number " +

            cr.Errors[i].Line + ": " +

            cr.Errors[i].ErrorText

          );

        }

        return null;

      }

      System.Reflection.Assembly a =

        cr.CompiledAssembly;

      object o =

        a.CreateInstance("VBCodeEval.VBCodeEval");

      Type t = o.GetType();

      MethodInfo mi = t.GetMethod("EvalCode");

      object s = mi.Invoke(o, null);

      return s;

    }

    [CommandMethod("EV")]

    public void Eval()

    {

      DocumentCollection dm =

          Application.DocumentManager;

      Editor ed = dm.MdiActiveDocument.Editor;

      const string csCode =

        "typeof(Autodesk.AutoCAD." +

        "ApplicationServices.Application)";

      const string vbCode =

        "GetType(Autodesk.AutoCAD." +

        "ApplicationServices.Application)";

      ed.WriteMessage("\nEvaluating C# code:\n" + csCode);

      object result = EvalCS(csCode);

      if (result != null)

        ed.WriteMessage(

          "\nC# code returned: " +

          result.ToString()

        );

      ed.WriteMessage("\nEvaluating VB code:\n" + vbCode);

      result = EvalVB(vbCode);

      if (result != null)

        ed.WriteMessage(

          "\nVB code returned: " +

          result.ToString()

        );

    }

  }

}

Here's the VB.NET code, which shows how to compile and execute C# (heterogeneous) and VB.NET (homogeneous) code:

Imports Autodesk.AutoCAD.Runtime

Imports Autodesk.AutoCAD.ApplicationServices

Imports Autodesk.AutoCAD.EditorInput

Imports Microsoft.CSharp

Imports Microsoft.VisualBasic

Imports System.CodeDom.Compiler

Imports System.Reflection

Imports System.Text

Imports System

Namespace Metaprogramming

  Public Class Commands

    Private Const acadFolder As String = _

      "c:\\Program Files\\Autodesk\\AutoCAD 2008\\"

    'EvalCS: Evaluates C# source

    Public Shared Function EvalCS(ByVal csCode As String) _

    As Object

      Dim dm As DocumentCollection = _

        Application.DocumentManager

      Dim ed As Editor = dm.MdiActiveDocument.Editor

      Dim cs As CSharpCodeProvider = New CSharpCodeProvider

      Dim cp As CompilerParameters = New CompilerParameters

      cp.ReferencedAssemblies.Add("system.dll")

      cp.ReferencedAssemblies.Add(acadFolder + "acdbmgd.dll")

      cp.ReferencedAssemblies.Add(acadFolder + "acmgd.dll")

      cp.CompilerOptions = "/t:library"

      cp.GenerateInMemory = True

      Dim sb As StringBuilder = New StringBuilder

      sb.Append("using System;" & vbLf)

      sb.Append("using Autodesk.AutoCAD.Runtime;" & vbLf)

      sb.Append( _

        "using Autodesk.AutoCAD.ApplicationServices;" & vbLf)

      sb.Append( _

        "using Autodesk.AutoCAD.DatabaseServices;" & vbLf)

      sb.Append("using Autodesk.AutoCAD.EditorInput;" & vbLf)

      sb.Append("using Autodesk.AutoCAD.Geometry;" & vbLf)

      sb.Append("namespace CSCodeEval{" & vbLf)

      sb.Append("public class CSCodeEval{" & vbLf)

      sb.Append("public object EvalCode(){" & vbLf)

      sb.Append("return " + csCode + ";" & vbLf)

      sb.Append("}" & vbLf)

      sb.Append("}" & vbLf)

      sb.Append("}" & vbLf)

      Dim cr As CompilerResults = _

        cs.CompileAssemblyFromSource(cp, sb.ToString)

      If (cr.Errors.Count > 0) Then

        ed.WriteMessage( _

          vbLf & "Errors evaluating C# code (" + _

          cr.Errors.Count.ToString + "):")

        Dim i As Integer

        For i = 0 To cr.Errors.Count - 1

          ed.WriteMessage( _

            vbLf & "Line number " + _

            cr.Errors(i).Line.ToString + ": " + _

            cr.Errors(i).ErrorText)

        Next

        Return Nothing

      End If

      Dim a As System.Reflection.Assembly = _

        cr.CompiledAssembly

      Dim o As Object = _

        a.CreateInstance("CSCodeEval.CSCodeEval")

      Dim t As Type = o.GetType

      Dim mi As MethodInfo = t.GetMethod("EvalCode")

      Dim s As Object = mi.Invoke(o, Nothing)

      Return s

    End Function

    'EvalVB: Evaluates VB source

    Public Shared Function EvalVB(ByVal vbCode As String) _

    As Object

      Dim dm As DocumentCollection = _

        Application.DocumentManager

      Dim ed As Editor = dm.MdiActiveDocument.Editor

      Dim vb As VBCodeProvider = New VBCodeProvider

      Dim cp As CompilerParameters = New CompilerParameters

      cp.ReferencedAssemblies.Add("system.dll")

      cp.ReferencedAssemblies.Add(acadFolder + "acdbmgd.dll")

      cp.ReferencedAssemblies.Add(acadFolder + "acmgd.dll")

      cp.CompilerOptions = "/t:library"

      cp.GenerateInMemory = True

      Dim sb As StringBuilder = New StringBuilder

      sb.Append("Imports System" & vbLf)

      sb.Append("Imports Autodesk.AutoCAD.Runtime" & vbLf)

      sb.Append( _

        "Imports Autodesk.AutoCAD.ApplicationServices" & vbLf)

      sb.Append( _

        "Imports Autodesk.AutoCAD.DatabaseServices" & vbLf)

      sb.Append("Imports Autodesk.AutoCAD.EditorInput" & vbLf)

      sb.Append("Imports Autodesk.AutoCAD.Geometry" & vbLf)

      sb.Append("Namespace VBCodeEval" & vbLf)

      sb.Append("Public Class VBCodeEval" & vbLf)

      sb.Append("Public Function EvalCode() As Object" & vbLf)

      sb.Append("Return " + vbCode + " " & vbLf)

      sb.Append("End Function" & vbLf)

      sb.Append("End Class" & vbLf)

      sb.Append("End Namespace" & vbLf)

      Dim cr As CompilerResults = _

        vb.CompileAssemblyFromSource(cp, sb.ToString)

      If (cr.Errors.Count > 0) Then

        ed.WriteMessage( _

          vbLf & "Errors evaluating VB code (" + _

          cr.Errors.Count.ToString + "):")

        Dim i As Integer

        For i = 0 To cr.Errors.Count - 1

          ed.WriteMessage( _

            vbLf & "Line number " + _

            cr.Errors(i).Line.ToString + ": " + _

            cr.Errors(i).ErrorText)

        Next

        Return Nothing

      End If

      Dim a As System.Reflection.Assembly = _

        cr.CompiledAssembly

      Dim o As Object = _

        a.CreateInstance("VBCodeEval.VBCodeEval")

      Dim t As Type = o.GetType

      Dim mi As MethodInfo = t.GetMethod("EvalCode")

      Dim s As Object = mi.Invoke(o, Nothing)

      Return s

    End Function

    <CommandMethod("EV")> _

    Public Sub Eval()

      Dim dm As DocumentCollection = _

        Application.DocumentManager

      Dim ed As Editor = dm.MdiActiveDocument.Editor

      Const csCode As String = _

        "typeof(Autodesk.AutoCAD." + _

        "ApplicationServices.Application)"

      Const vbCode As String = _

        "GetType(Autodesk.AutoCAD." + _

        "ApplicationServices.Application)"

      ed.WriteMessage( _

        vbLf + "Evaluating C# code:" + _

        vbLf + csCode)

      Dim result As Object = EvalCS(csCode)

      If (Not result Is Nothing) Then

        ed.WriteMessage( _

          vbLf + "C# code returned: " + _

          result.ToString)

      End If

      ed.WriteMessage( _

        vbLf + "Evaluating VB code:" + _

        vbLf + vbCode)

      result = EvalVB(vbCode)

      If (Not result Is Nothing) Then

        ed.WriteMessage( _

          vbLf + "VB code returned: " + _

          result.ToString)

      End If

    End Sub

  End Class

End Namespace

When we run the "ev" command, implemented by either of the above code fragments, we see these results:

Command: ev

Evaluating C# code:

typeof(Autodesk.AutoCAD.ApplicationServices.Application)

C# code returned: Autodesk.AutoCAD.ApplicationServices.Application

Evaluating VB code:

GetType(Autodesk.AutoCAD.ApplicationServices.Application)

VB code returned: Autodesk.AutoCAD.ApplicationServices.Application

  1. Very good stuff. If you get a chance take a look at stuff I've been messing around with in C# and IronPython as you might find it interesting.

    code.google.com/p/py...

  2. Excellent post. I am a SolidWorks bigot / Autodesk critic but this is a great post. I look forward to trying this.

  3. "The quotations mechanism in F# appears to be the way to represent, analyse and execute program structure. This article describes the concepts, although it's quite deep and doesn't address the case that's most immediately interesting to AutoCAD develeopers: the ability to evaluate and execute code represented as a string."

    Hi Kean.

    I found this comment interesting, as I haven't come across many situations where the ability to evaluate and execute code represented as a string was useful.

    I have done my share of 'metaprogramming' in AutoLISP (and also with the formalization of metaprogramming in Common LISP - defmacro), but none of that involved manipulating source code in string form.

    Perhaps you could show or cite a practicle example of where evaluating and executing code stored as a string is useful to AutoCAD programmers?

    Thanks,
    Tony

  4. Hi Tony,

    The instances I've encountered this are typically when code fragments are stored in a DWG or an external database, to be retrieved and executed as needed.

    Let me refer you to the comment that started this topic off, as that may help. Search for the comment from Thomas on this page.

    Regards,

    Kean

  5. You've been kicked (a good thing) - Trackback from CadKicks.com
    cadkicks.com/adkauto...

Leave a Reply to Tony Tanzillo Cancel reply

Your email address will not be published. Required fields are marked *