4

Delphi Prism and the .NET Framework are both extremely powerful tools and bring a lot of flexibility that Delphi for Win32 cannot necessarily provide, particularly when it comes to reflection and code generation. I recently started a project where I wanted to be able to compile a string with Delphi Prism code into an assembly without hacking my way around with the Delphi Prism command line compiler.  This could be used to potentially create a Delphi Prism Snippet Compiler similar to the .NET Snippet Compiler from Jeff Key.

It is my intention that this post should introduce you to the process of dynamically compiling code and that the next part will hopefully be an introduction to the process of dynamically generating and compiling code using the .NET CodeDom.

To begin with, create a New Console Application in Delphi Prism and Add a Reference to the RemObjects.Oxygene.CodeModel assembly. For my installation, this was found in the Bin directory of the Delphi Prism program files directory.

Once you’ve done this you can use the following code:

namespace DynamicCompilation;

interface

uses
  System.Collections,
  System.CodeDom.Compiler,
  RemObjects.Oxygene.CodeDom;

type
  ConsoleApp = class
  public
    class method Main;
  end;

implementation

class method ConsoleApp.Main;
var
  provider: CodeDomProvider;
  strFileOut: string;
  results: CompilerResults;
  cp: CompilerParameters;
  strSource: String;
begin

strSource := "namespace HelloWorld;

interface

type
  ConsoleApp = public class
  public
    class method Main;
  end;

implementation

class method ConsoleApp.Main;
begin
  Console.WriteLine('Hello World.');
end;
end.";

  // Get our CodeDomProvider
  provider := CodeDomProvider.CreateProvider('Oxygene');

  cp := new System.CodeDom.Compiler.CompilerParameters;
  cp.CompilerOptions := '';
  // Defaults to false which creates a dll instead.
  cp.GenerateExecutable := true;
  strFileOut := 'C:\test.exe';
  cp.OutputAssembly := strFileOut;

  // Compile our code.
  results := provider.CompileAssemblyFromSource(cp, [strSource]);

  Console.WriteLine('Number of Errors Encountered: {0}', results.Errors.Count);
  for error: CompilerError in results.Errors do
      begin
        Console.WriteLine('{0}: {1}', [error.ErrorNumber, error.ErrorText]);
      end;
  if (Assigned(results.PathToAssembly)) then
     begin
       Console.WriteLine('Path to Assembly: {0}', results.PathToAssembly);
     end;

  Console.WriteLine('Press Enter to exit..');
  Console.ReadLine;
end;

end.
Our Test Console Output

Our Test Console Output

This code should be fairly straight forward to understand but there are a few parts that are worth drawing attention to. The first is this line:

provider := CodeDomProvider.CreateProvider('Oxygene');

I originally tried to create a new instance of the RemObjects.Oxygene.CodeModel.OxygeneCodeProvider as you can when creating a CSharpCodeProvider:

            var provider_options = new Dictionary
                         {
                             {"CompilerVersion","v3.5"}
                         };
            var provider = new Microsoft.CSharp.CSharpCodeProvider(provider_options);

However in my experience calling the constructor on the OxygeneCodeProvider class merely causes a Null Reference Exception when you actually try to use a Compile method on the resulting class. It is worth noting that by replacing the string ‘Oxygene’ above with ‘VisualBasic’ or ‘CSharp’ you can obtain an instance of other language providers.

The next part worth paying careful attention to is the setup of the CompilerParameters:

  cp := new System.CodeDom.Compiler.CompilerParameters;
  cp.GenerateExecutable := true;
  strFileOut := 'C:\test.exe';
  cp.OutputAssembly := strFileOut;

This part of the code, configures the Provider to generate an executable instead an assembly (which is the default configuration) and tells the Provider where to output the resulting executable. This should be relatively straightforward and for a full reference of CompilerParameters – see the MSDN documentation on System.CodeDom.Compiler.Compilerparameters.

Those sharp-eyed amongst you may have noticed that the code that I’m compiling above does not contain any uses declarations. First we need to Reference the relevant assemblies using the CompilerParameters.ReferencedAssemblies StringCollection:

  cp := new System.CodeDom.Compiler.CompilerParameters;
  // Clear our Assembly List
  cp.ReferencedAssemblies.Clear;
  // Add a few usual suspects
  cp.ReferencedAssemblies.Add('System.dll');
  cp.ReferencedAssemblies.Add('System.Core.dll');

You can then “uses” any namespace in the referenced assemblies (be careful: There is no official .NET assembly called System.Linq.dll for example).

The final part which is worth drawing attention to is the actual compile command:

   results := provider.CompileAssemblyFromSource(cp, [strSource]);

For reference, the relevant parts of the declaration for a CodeDomProvider might look something like the following:

public CodeDomProvider = class abstract(Component)
    public function CompileAssemblyFromDom(options: CompilerParameters; [ParamArray] compilationUnits: CodeCompileUnit[]): CompilerResults; virtual;
    public function CompileAssemblyFromFile(options: CompilerParameters; [ParamArray] fileNames: string[]): CompilerResults; virtual;
    public function CompileAssemblyFromSource(options: CompilerParameters; [ParamArray] sources: string[]): CompilerResults; virtual;

You can feed the CompileAssemblyFrom methods several different files, strings, or Dom structures which might represent different .pas files that you would create when you created a project in Delphi Prism. The example below includes an additional class:

namespace DynamicCompilation;

interface

uses
  System.Collections,
  System.CodeDom.Compiler,
  RemObjects.Oxygene.CodeDom;

type
  ConsoleApp = class
  public
    class method Main;
  end;

implementation

class method ConsoleApp.Main;
var
  provider: CodeDomProvider;
  strFileOut: string;
  results: CompilerResults;
  cp: CompilerParameters;
  strSource: String;
begin

strSource := "namespace HelloWorld;

interface

type
  ConsoleApp = public class
  public
    class method Main;
  end;

implementation

class method ConsoleApp.Main;
var
  PingPong: TPingPong;
begin
  PingPong := new TPingPong;
  Console.WriteLine(PingPong.Ping);
  Console.WriteLine('Hello World.');
end;
end.";

var strSource2 := "namespace HelloWorld;

interface

type
  TPingPong = public class
  public
    method Ping: string;
  end;

implementation

method TPingPong.Ping: string;
begin
  Result := 'PONG!';
end;
end.";

  // Get our CodeDomProvider
  provider := CodeDomProvider.CreateProvider('Oxygene');

  cp := new System.CodeDom.Compiler.CompilerParameters;

  cp.CompilerOptions := '';
  // Defaults to false which creates a dll instead.
  cp.GenerateExecutable := true;
  strFileOut := 'C:\test.exe';
  cp.OutputAssembly := strFileOut;

  // Clear our Assembly List
  cp.ReferencedAssemblies.Clear;
  // Add a few usual suspects
  cp.ReferencedAssemblies.Add('System.dll');
  cp.ReferencedAssemblies.Add('System.Data.dll');

  // Compile our code.
  results := provider.CompileAssemblyFromSource(cp, [strSource2, strSource]);

  //Console.WriteLine('Generated assembly name: ' + results.CompiledAssembly.FullName);
  Console.WriteLine('Number of Errors Encountered: {0}', results.Errors.Count);
  for error: CompilerError in results.Errors do
      begin
        Console.WriteLine('{0}: {1}', [error.ErrorNumber, error.ErrorText]);
      end;
  if (Assigned(results.PathToAssembly)) then
     begin
       Console.WriteLine('Path to Assembly: {0}', results.PathToAssembly);
     end;

  Console.WriteLine('Press Enter to exit..');
  Console.ReadLine;
end;

end.

And voila.. the output from our dynamically compiled executable can be seen as below:

The output from our dynamically generated executable

The output from our dynamically generated executable

If the other compiler methods available with our CodeProvider caught your eye then this leads me nicely onto my intended next steps. While generating code from a string containing your code is nice and easy, sometimes it may not be the best way to approach Code Generation.

I realise that the end result of the above could have been achieved by simply writing a file out and then executing the command line compiler but where’s the fun in that? Next time I intend on looking at generating a CodeDom tree and providing this to the Compiler (no more potentially troublesome string manipulation required).

Tags: , ,

4 Comments

  1. Lex Li on the 3rd September 2009 remarked #

    Better check out Mono C# Shell, that goes further.

  2. jamiei on the 3rd September 2009 remarked #

    @Lex Li – The Mono C# Shell is very nice and a useful tool (http://www.mono-project.com/CsharpRepl), I would imagine that something similar with Delphi Prism is very possible. Could be a good side project for the guys at RemObjects.

  3. jeu psp on the 23rd September 2009 remarked #

    Hi…
    I know .Net very well but now i need to learn Delphi Prism also. I also started a new project and i have to gain knowledge with in 15 days. Thank you so much for giving extra informations which i really required.

  4. Mark Johnson on the 8th August 2011 remarked #

    I recently told to start programming for .NET including for Linux environment. So, I am programming and learning Delphi Prism for the last 3 months. It definitely has been a true learning experience. However, I should give credits to people like you who post very important and great programming articles like this one. Thank you.

Leave a Comment