Library for Inline C# in MSH

Wed, Dec 14, 2005 4-minute read

In the last post, we got nearly as far as we could in improving the performance of our MSH script.  We used the profiler to help target our performance optimizations.  After we were finished, setting variables, incrementing counters, and comparing colour values took up the vast majority of our time.  We can’t make these statements more efficient, nor can we execute them less frequently.

To really push the performance of this script, we’ll write the highly critical sections using inline C#, rather than MSH.

This interop idea is something that many people end implement on their own after playing with .Net (and MSH) for any length of time.  I use it extensively in my C# Performance Comparison tool, MOW wrote it for VB in MSH, Jeffrey Snover wrote it for C# in MSH (as did half of the rest of the Monad team,) and Bruce Payette even wrote one for MSIL in MSH.

Normally, I would just link to an implementation of “C# in MSH,” but I’m going to offer a library function that goes a bit further:

  1. The inline C# accepts dynamic arguments, and returns dynamic values.  Other implementations hard-code the parameters, and can’t interact with the rest of the script.
  2. The inlining mechanism caches the temporary compiled C# class.  No matter how often you call the same inline code in a script, you will only suffer the (relatively minor) compilation burden once.
  3. The inlined code does not require any class definitions, namespace imports, or other template code.

It is commented in excruciating detail below.  Don’t worry, you’ll have a nice speedy fire warming up your console in no time flat.

[Edit: A better way to get the installation path of PowerShell is $psHome]
[Edit: Updated to work with RTM builds]
[Edit: Added ability to reference other DLLs]

[Edit: This is now built into PowerShell as the Add-Type cmdlet ]

################################################################################ 
## Invoke-Inline.ps1
## Library support for inline C# 
## 
## Usage 
##  1) Define just the body of a C# method, and store it in a string.  "Here 
##     strings" work great for this.  The code can be simple: 
## 
##     $codeToRun = "Console.WriteLine(Math.Sqrt(337));" 
## 
##     or more complex: 
## 
##     $codeToRun = @" 
##         string firstArg = (string) ((System.Collections.ArrayList) arg)[0]; 
##         int secondArg = (int) ((System.Collections.ArrayList) arg)[1]; 
## 
##         Console.WriteLine("Hello {0} {1}", firstArg, secondArg ); 
##      
##         returnValue = secondArg * 3; 
##     "@ 
## 
##  2) (Optionally) Pack any arguments to your function into a single object. 
##     This single object should be strongly-typed, so that PowerShell does
##     not treat  it as a PsObject. 
##     An ArrayList works great for multiple elements.  If you have only one  
##     argument, you can pass it directly. 
##    
##     [System.Collectionts.ArrayList] $arguments =
##         New-Object System.Collections.ArrayList 
##     [void] $arguments.Add("World") 
##     [void] $arguments.Add(337) 
## 
##  3) Invoke the inline code, optionally retrieving the return value.  You can 
##     set the return value in your inline code by assigning it to the 
##     "returnValue" variable as shown above. 
## 
##     $result = Invoke-Inline $codeToRun $arguments 
## 
## 
##     If your code is simple enough, you can even do this entirely inline: 
## 
##     Invoke-Inline "Console.WriteLine(Math.Pow(337,2));" 
##   
################################################################################ 
param(
    [string] $code,
    [object] $arg,
    [string[]] $reference = @()
    )

## Stores a cache of generated inline objects.  If this library is dot-sourced 
## from a script, these objects go away when the script exits. 
if(-not (Test-Path Variable:\lee.holmes.inlineCache))
{
    ${GLOBAL:lee.holmes.inlineCache} = @{}
}

## The main function to execute inline C#.   
## Pass the argument to the function as a strongly-typed variable.  They will  
## be available from C# code as the Object variable, "arg". 
## Any values assigned to the "returnValue" object by the C# code will be  
## returned to MSH as a return value. 

function main
{
    ## See if the code has already been compiled and cached 
    $cachedObject = ${lee.holmes.inlineCache}[$code]

    ## The code has not been compiled or cached 
    if($cachedObject -eq $null)
    {
        $codeToCompile =
@"
    using System;

    public class InlineRunner
    {
        public Object Invoke(Object arg)
        {
            Object returnValue = null;

            $code

            return returnValue;
        }
    }
"@

        ## Obtains an ICodeCompiler from a CodeDomProvider class. 
        $provider = New-Object Microsoft.CSharp.CSharpCodeProvider

        ## Get the location for System.Management.Automation DLL 
        $dllName = [PsObject].Assembly.Location

        ## Configure the compiler parameters 
        $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters

        $assemblies = @("System.dll", $dllName)
        $compilerParameters.ReferencedAssemblies.AddRange($assemblies)
        $compilerParameters.ReferencedAssemblies.AddRange($reference)
        $compilerParameters.IncludeDebugInformation = $true
        $compilerParameters.GenerateInMemory = $true

        ## Invokes compilation.  
        $compilerResults =
            $provider.CompileAssemblyFromSource($compilerParameters, $codeToCompile)

        ## Write any errors if generated.         
        if($compilerResults.Errors.Count -gt 0)
        {
            $errorLines = ""
            foreach($error in $compilerResults.Errors)
            {
                $errorLines += "`n`t" + $error.Line + ":`t" + $error.ErrorText
            }
            Write-Error $errorLines
        }
        ## There were no errors.  Store the resulting object in the object 
        ## cache. 
        else
        {
            ${lee.holmes.inlineCache}[$code] =
                $compilerResults.CompiledAssembly.CreateInstance("InlineRunner")
        }

        $cachedObject = ${lee.holmes.inlineCache}[$code]
   }

   ## Finally invoke the C# code 
   if($cachedObject -ne $null)
   {
       return $cachedObject.Invoke($arg)
   }
}

. Main

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]