PowerShell Cookbook

Search

Categories

 

On this page

How do I easily load assemblies, when LoadWithPartialName has been deprecated?
Now THAT is obfuscated!
Monad hosting, an introduction
New York Times follow-up on Brooklyn Camera Dealers
Question: Do you _not_ read the newsgroup?
Add a high-fidelity music input source to your car
Neat Integration of CodeSmith, MSBuild, Monad and Windows Workflow
30 Year Vintage Computer Collection on EBay
burn-console: Optimized, and Ready For Marshmallows
Library for Inline C# in MSH

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 216
This Year: 16
This Month: 0
This Week: 0
Comments: 523

Sign In

 Tuesday, January 17, 2006
Tuesday, January 17, 2006 6:31:10 PM (Pacific Standard Time, UTC-08:00) ( )

There are some very useful parts of the .Net framework (ie: System.Web and System.Windows.Forms) that Monad doesn’t load into its AppDomain by default.  In order to use them, you first load them with one of the Load() methods from the System.Reflection.Assembly class.

At first, the easiest approach seems to be LoadWithPartialName.  You can simply request “System.Windows.Forms” and be done with it:

MSH:73 C:\temp > [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

GAC    Version        Location
---    -------        --------
True   v2.0.50727     C:\WINNT\assembly\GAC_MSIL\System.Windows.Forms\2.0.0.0__b77a5c561934e...

(you can cast the result to [void] if you don’t want your script to belch the loading information.)

However, the documentation shows this handy method as being deprecated.

This is because the API needs to make too many guesses about what you really want.  Most people don't really want "Whatever version of System.Windows.Forms you can scrounge up from the GAC or current directory."  Most people really want "Microsoft's version of System.Windows.Forms, shipped with .Net 2.0"

This is especially true in the face of breaking changes that may or may not happen to the DLL you want to load.  For applications that need to be reliable, LoadWithPartialName will cause problems.  Suzanne Cook discusses its implications on her blog entry, “Avoid Partial Binds.”

For scripts that you don't mind debugging by hand when things break, you can stick with LoadWithPartialName until it's physically removed from the framework.  However, your best bet is to consciously make the decisions that the LoadWithPartialName method makes on your behalf with the following helper script:

################################################################################
## load-assembly.msh
##
## Loads a given assembly by a more friendly name, while still using the strong
## binding characteristics of Assembly.Load.
##
## Assembly.LoadWithPartialName has been deprecated, as it binds only by display
## name.  It's a convenient shortcut, but opens your application and script, and
## environment  to all sorts of reliability issues, including:
##    backwards incompatibility, forwards incompatibility, breaking changes,
##    and subtle assembly dependency problems.
##
################################################################################
param([string] $assemblyName)

## Our assembly name shortcuts
$assemblyMappings = (
   ("forms""System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"),
   ("web""System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
)

## List the assembly shortcuts we support
if(-not $assemblyName)
{
   "Please specify an assembly name.  Supported assemblies are: "
   foreach($assembly in $assemblyMappings) { $assembly[0] }
   return
}

## Load the assembly they request
## This fails with an error message if this specific assembly version can't
## be loaded.
foreach($assembly in $assemblyMappings)
{
    if($assemblyName -eq $assembly[0])
    {
        [void] [Reflection.Assembly]::Load($assembly[1])
    }
}



 

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

Comments [4] | | # 
 Friday, January 13, 2006
Saturday, January 14, 2006 12:43:27 AM (Pacific Standard Time, UTC-08:00) ( )

You might remember a recent obfuscated Monad script that Adam wrote.  It's impressive, but not as obfuscated as this one

This turned up in a search I was doing -- feed2podcast.com looks like an interesting idea.  Thank goodness they don't index the comments, because the spam sure is piling high there, Adam :)

 

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

Comments [0] | | # 
Saturday, January 14, 2006 12:08:48 AM (Pacific Standard Time, UTC-08:00) ( )

A topic came up on the newsgroup today that I thought was a good segue into a series of posts that I’ve been working on.  Paraphrased, the question was, “How do I get Monad to do stuff from another .Net language?”

There are two ways to go about this, depending on the requirement.

The first is to cleanly separate your object model from your cmdlet implementation.  You write a class that does what you need it to.  You can invoke that class from .Net like any other class, and write your Cmdlet as a thin wrapper to interact with that same object model.  Think of this as the business logic / UI separation that is so important in designing a good application.

This is the ideal solution if your task and object model do not require access to the facilities that the Monad hosting environment provides.  Those facilities include object pipelines, user interaction, other cmdlets, providers, for example.

For cmdlets that do not require access to the Monad hosting environment, the release notes for Beta 3 describe a technique of accessing them from other .Net languages.  Although it is possible, it is really a release valve.  When access to the cmdlet from outside of Monad is important, developers should strongly prefer to expose both their object model and thin cmdlet wrapper to users.

The second way is to host the Monad engine inside of your application, and have Monad execute the pipelines you specify.  This provides both their application, and their cmdlets with many of the other benefits that come along with the Monad hosting environment.

This latter approach is one that I will illustrate over the next few articles.

 

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

Comments [1] | | # 
 Wednesday, January 11, 2006
Wednesday, January 11, 2006 6:47:16 PM (Pacific Standard Time, UTC-08:00) ( )

I had a phone interview with Michael Brick at the New York Times a few weeks ago about the problems I (and others) have had with certain shady online camera dealers.

His article on that just got printed, and it’s a good read.

Especially interesting is the various official quotes that explain how difficult it is to solve the problem.  Our collective internet rage takes place in a thought-based medium, where anything is possible.  We CC company CEOs on complaint emails, conduct virtual letter writing campaigns, and many other grand schemes for enacting change.  The transition to reality isn’t nearly as high-fidelity, and illustrates a sharp contrast:

"It is a perennial problem for us, particularly in New York, not necessarily for New York customers but for customers around the country," said Anthony Barbera, manager of the Information and Investigations Department of the Better Business Bureau of New York. "It is kind of a gray area, and we don't have enforcement power."

Keeping track of the companies has been a challenge, Mr. Barbera said, because they shift identities, shedding each Web site as it gains a bad reputation.

Without evidence of outright fraud, law enforcement agencies have largely declined to pursue criminal charges. This year, the state attorney general, Eliot Spitzer, mediated four complaints against Price Rite and referred two to the city Department of Consumer Affairs.

"It's just not something we have the resources to pursue, in terms of you need a pattern and you need a number of complaints and a similarity in the pattern in order to put an action together," said Brad Maione, a spokesman for Mr. Spitzer.

However, these places are in business to make money in our thought-based medium.  Blogs and online forums (indexed by the same search engines that drive traffic to these retailers) enable us to choke their supply of customers.  Word of mouth has never been so efficient.

Like mine, Thomas Hawk’s posts, and Don Wiss’ efforts have certainly been the cause of thousands of lost sales for these companies.

[Aside: As somebody correctly points out in Thomas’ comments, my quote in the article uses the word ‘literally’ very incorrectly.  I’m pretty prudish when it comes to that word (and hate its abuse,) so I think it was an incorrect paraphrase.  I could be wrong, though – it was a telephone conversation.]

 

 

Comments [0] | | # 
Wednesday, January 11, 2006 9:45:44 AM (Pacific Standard Time, UTC-08:00) ( )

The microsoft.public.windows.server.scripting newsgroup has really blossomed into a useful community for Monad users.  When I post lengthy responses to the newsgroup, I very rarely post them to this blog as well.

So the question is -- how many of you do not read the newsgroup?  Feel free to post your response in either email, or a comment below.  If many of you read only the blogs, then I'll start posting more entries based on newsgroup threads.

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

Comments [19] | | # 
 Monday, January 09, 2006
Monday, January 09, 2006 6:43:12 PM (Pacific Standard Time, UTC-08:00) ( )

I'm rebuilding my computer, so I don't have access to anything I have been planning to blog.  So instead, I thought I'd write about something that is commonly considered difficult -- but is actually quite within the reach of most of us.  Adding a high-fidelity music input source to your car.

If you have an MP3 player (or portable satellite receiver,) the first thing most people try is adding broadcasting the signal to their car stereo via an FM transmitter.  Transmitters designed for personal use are terrible, even under ideal conditions.  When I tried this, I managed to improve reception somewhat by placing the transmitter at the end of a long headphone extension cable.  This lets you place the transmitter as close to the car’s antenna as possible, and also acts as an antenna to increase the signal strength of the FM transmitter.  Doc Searls talks a little bit more about that here.

However, even ideal FM radio is still just FM radio.  Passable sound quality, but not that great, as it is approximately equal to that of a 56 kbps MP3.  The solution?  Add an RCA input to your car stereo.

You can usually get an adapter for about $30, and the “RCA to Mini” cord for about $15.  They plug into the place where the CD changer usually plugs into.  If you have a CD changer already, some will work as a pass-through.  If you are very lucky, your stereo already has the inputs and you just need to get the RCA to Mini cable.
If you have a stock radio in your car, searches like

            “<car name> RCA adapter”
            “<car name> auxiliary input adapter”

should help find the part to do what you need.  If that doesn’t turn up anything (or you have an aftermarket radio,) search for the input adapter that matches its model number.

Installation is usually pretty simple.  Radios are designed to come out pretty easily, so you can usually do it yourself.  It’s easily within the capabilities of somebody that can change a hard-drive in their computer, or build some IKEA furniture.  Just remember not to force anything that doesn’t want to go :)  Depending on your model of car, you can usually find plenty of places on the internet that will show how to gain access to the back panel.  The term car junkies use for the radio is “head unit,” so something like

            “replace head unit <car name>”

should give you an overview.  You’re not replacing it, but the instructions will show you how to get access to the back of the radio.  However, if your dash has a lot of pretty custom molding (or you just don’t want to be bothered,) a place like Best Buy should be able to install the part for about $30 in labour.  If you live in the Seattle area, I’ve had a lot of stereo work done at the Best Buy in Bellevue, and I trust their work.

Comments [0] | | # 
 Tuesday, January 03, 2006
Tuesday, January 03, 2006 5:36:11 PM (Pacific Standard Time, UTC-08:00) ( )

Howard van Rooijen, who works for Conchango, just posted a neat integration of CodeSmith, MSBuild, Monad and Windows Workflow for his company’s Community Day.  We spoke about integrating MSBuild with Monad during the Hands on Labs at the PDC (and a few times since,) and it’s cool to see what’s come of his tinkering.

His Congestion Charge Demo simulates London’s Vehicle Congestion Charging System – the automated system that charges a premium for driving within central London during peak hours.

Great stuff!

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

Comments [0] | | # 
 Monday, December 19, 2005
Monday, December 19, 2005 6:54:33 PM (Pacific Standard Time, UTC-08:00) ( )

Somebody is selling their gigantic 30 year vintage computer collection as a single lot on EBay

At several hundred collectible computers, and nearly as many manuals, this is a really nice trip down memory lane.  Gotta get me one of those laptops!

Comments [0] | | # 
 Sunday, December 18, 2005
Sunday, December 18, 2005 8:57:13 PM (Pacific Standard Time, UTC-08:00) ( )

In the last article, I introduced a library function to help you invoke inline C# from your scripts.  Since we carefully profiled the burn-console script’s performance, we know exactly where to apply these inline performance optimizations: to the updateBuffer function, and the updateScreen function.

The completed script is included below. The inline optimizations bring us to about 30 - 40 frames per second, easily fast enough to provide smooth animation.

 
###############################################################################
## burn-console.ps1
##
## Create a fire effect in PowerShell, using the Console text buffer as the 
## rendering surface.
##
## Great overview of the fire effect algorithm here: 
## http://freespace.virgin.net/hugo.elias/models/m_fire.htm
##
###############################################################################

function main
{
    write-debug "ENTER main"
    ## Rather than a simple red fire, we'll introduce oranges and yellows
    ## by including Yellow as one of the base colours
    $colours = "Yellow","Red","DarkRed","Black"
    
    ## The four characters that we use to dither with, along with the 
    ## percentage of the foreground colour that they show
    $dithering = "█","▓","▒","░"
    $ditherFactor = 1,0.75,0.5,0.25
    
    ## Hold the palette.  We actually store each entry as a BufferCell,
    ## since we need to retain a foreground colour, background colour,
    ## and dithering character.
    [System.Management.Automation.Host.BufferCell[]] $palette = `
        new-object System.Management.Automation.Host.BufferCell[] 256
    
    ## Resize the console to 70, 61 so we have a consistent buffer
    ## size for performance comparison.
    # $bufferSize = new-object System.Management.Automation.Host.Size 70,61
    # $host.UI.RawUI.WindowSize = $bufferSize

    ## Retrieve some commonly used dimensions
    $windowWidth = $host.UI.RawUI.WindowSize.Width
    $windowHeight = $host.UI.RawUI.WindowSize.Height
    $origin = `
        new-object System.Management.Automation.Host.Coordinates 0,0
    $dimensions = `
        new-object System.Management.Automation.Host.Rectangle `
            0,0,$windowWidth,$windowHeight
    
    ## Create our random number generator
    $random = new-object Random
    $workingBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)
    $screenBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)

    ## Store a reference to the Invoke-Inline script to save lookup time
    ## since we run it so often.
    $inline = Get-Command Invoke-Inline.ps1
    
    clear-host
    
    ## Generate the palette
    generatePalette
    # displayPalette
    # return;

    ## Update the buffer, then update the screen until the user presses a key.  
    ## Keep track of the total time and frames generated to let us display
    ## performance statistics.
    $frameCount = 0
    $totalTime = measure-command {
        while(! $host.UI.RawUI.KeyAvailable)
        {
            updateBuffer
            updateScreen
            $frameCount++
        }
    }
    
    ## Clean up and exit
    $host.UI.RawUI.ForegroundColor = "Gray"
    $host.UI.RawUI.BackgroundColor = "Black"
    
    write-host
    write-host "$($frameCount / $totalTime.TotalSeconds) frames per second."
    write-debug "EXIT"
}

## Update a back-buffer to hold all of the information we want to display on
## the screen.  To do this, we first re-generate the fire pixels on the bottom 
## row.  With that done, we visit every pixel in the screen buffer, and figure
## out the average heat of its neighbors.  Once we have that average, we move
## that average heat one pixel up.
function updateBuffer
{
    ## This function takes the most of our time, so we'll do it inline.
    ## Inputs:
    ##  Window Height
    ##  Window Width
    ##  Screen Buffer
    ##  Random Number Generator
    ## Output:
    ##  Working Buffer
    
    [System.Collections.ArrayList] $inputs = `
        new-object System.Collections.ArrayList
    [void] $inputs.Add([int] $windowHeight)
    [void] $inputs.Add([int] $windowWidth)
    [void] $inputs.Add([int[]] $screenBuffer)
    [void] $inputs.Add([System.Random] $random)
    
    $code = @"
    // Unpack the inputs from our input object
    int windowHeight = (int) ((System.Collections.ArrayList) arg)[0];
    int windowWidth = (int) ((System.Collections.ArrayList) arg)[1];
    int[] screenBuffer = (int[]) ((System.Collections.ArrayList) arg)[2];
    Random random = (Random) ((System.Collections.ArrayList) arg)[3];
    
    // Start fire on the last row of the screen buffer
    for(int column = 0; column < windowWidth; column++)
    {
        // There is an 80% chance that a pixel on the bottom row will
        // start new fire.
        if(random.NextDouble() >= 0.20)
        {
            // The chosen pixel gets a random amount of heat.  This gives
            // us a lot of nice colour variation.
            screenBuffer[(windowHeight - 2) * (windowWidth) + column] = 
                (int) (random.NextDouble() * 255);
        }
    }
    
    int[] tempWorkingBuffer = (int[]) screenBuffer.Clone();
    
    // Propigate the fire
    int baseOffset = windowWidth + 1;
    for(int row = 1; row < (windowHeight - 1); row++)
    {
        for(int column = 1; column < (windowWidth - 1); column++)
        {
            // Get the average colour from the four pixels surrounding
            // the current pixel
            double colour = 
                (
                    screenBuffer[baseOffset] + 
                    screenBuffer[baseOffset - 1] + 
                    screenBuffer[baseOffset + 1] + 
                    screenBuffer[baseOffset + windowWidth]
                 ) / 4.0;

            // Cool it off a little.  We apply uneven cooling, otherwise
            // the cool dark red tends to stretch up for too long.
            if(colour > 0)
            {
                if(colour > 70)
                { 
                    colour -= 1
                }
                else
                {
                    colour -= 3;
                    
                    if(colour < 1)
                    {
                        colour = 0;
                    }
                    else if(colour < 20)
                    {
                        colour -= 1;
                    }
                }
            }

            // Store the result into the previous row -- that is, one buffer 
            // cell up.
            tempWorkingBuffer[baseOffset - windowWidth] = (int) colour;
            baseOffset ++;
        }
        
        baseOffset += 2;
    }

    returnValue = tempWorkingBuffer;
"@

    $returned = & $inline $code $inputs
    $SCRIPT:workingBuffer = $returned
}

## Take the contents of our working buffer and blit it to the screen
## We do this in one highly-efficent step (the SetBufferContents) so that
## users don't see each individial pixel get updated.
function updateScreen
{
    write-debug "ENTER updateScreen"
    
    ## This function takes up a lot of time, so we'll do it inline.
    ## Inputs:
    ##  host.UI.RawUI
    ##  palette
    ##  workingBuffer
    ##  origin
    ##  dimensions
    ##  windowHeight
    ##  windowWidth
    ## Output:
    ##  None
    
    [System.Collections.ArrayList] $inputs = `
        new-object System.Collections.ArrayList
    [void] $inputs.Add([System.Management.Automation.Host.PSHostRawUserInterface] $host.UI.RawUI)
    [void] $inputs.Add([System.Management.Automation.Host.BufferCell[]] $palette)
    [void] $inputs.Add([int[]] $workingBuffer)
    [void] $inputs.Add([System.Management.Automation.Host.Coordinates] $origin)
    [void] $inputs.Add([System.Management.Automation.Host.Rectangle] $dimensions)
    [void] $inputs.Add([int] $windowHeight)
    [void] $inputs.Add([int] $windowWidth)
    
    $code = @"

    System.Management.Automation.Host.PSHostRawUserInterface rawUI = 
        (System.Management.Automation.Host.PSHostRawUserInterface)
            ((System.Collections.ArrayList) arg)[0];
    System.Management.Automation.Host.BufferCell[] palette =
        (System.Management.Automation.Host.BufferCell[]) 
            ((System.Collections.ArrayList) arg)[1];
    int[] workingBuffer = 
        (int[]) ((System.Collections.ArrayList) arg)[2];
    System.Management.Automation.Host.Coordinates origin = 
        (System.Management.Automation.Host.Coordinates)
            ((System.Collections.ArrayList) arg)[3];
    System.Management.Automation.Host.Rectangle dimensions = 
        (System.Management.Automation.Host.Rectangle)
            ((System.Collections.ArrayList) arg)[4];
    int windowHeight = (int) ((System.Collections.ArrayList) arg)[5];
    int windowWidth = (int) ((System.Collections.ArrayList) arg)[6];

    // Create a working buffer to hold the next screen that we want to
    // create.
    System.Management.Automation.Host.BufferCell[,] nextScreen = 
        rawUI.GetBufferContents(dimensions);
    
    // Go through our working buffer (that holds our next animation frame)
    // and place its contents into the buffer that we will soon blast into
    // the real RawUI
    for(int row = 0; row < windowHeight; row++)
    {
        for(int column = 0; column < windowWidth; column++)
        {
            nextScreen[row, column] = palette[workingBuffer[(row * windowWidth) + column]];
        }
    }
    
    // Bulk update the RawUI's buffer with the contents of our next screen
    rawUI.SetBufferContents(origin, nextScreen);
"@
    & $inline $code $inputs
    
    ## And finally update our representation of the screen buffer to hold
    ## what actually is on the screen
    $SCRIPT:screenBuffer = $workingBuffer.Clone()

    write-debug "EXIT"
}

## Generates a palette of 256 colours.  We create every combination of 
## foreground colour, background colour, and dithering character, and then
## order them by their visual intensity.
##
## The visual intensity of a colour can be expressed by the NTSC luminance 
## formula.  That formula depicts the apparent brightness of a colour based on 
## our eyes' sensitivity to different wavelengths that compose that colour.
## http://en.wikipedia.org/wiki/Luminance_%28video%29
function generatePalette
{
    ## The apparent intensities of our four primary colours.
    ## However, the formula under-represents the intensity of our straight
    ## red colour, so we artificially inflate it.
    $luminances = 225.93,106.245,38.272,0
    $apparentBrightnesses = @{}

    ## Cycle through each foreground, background, and dither character
    ## combination.  For each combination, find the apparent intensity of the 
    ## foreground, and the apparent intensity of the background.  Finally,
    ## weight the contribution of each based on how much of each colour the
    ## dithering character shows.
    ## This provides an intensity range between zero and some maximum.
    ## For each apparent intensity, we store the colours and characters
    ## that create that intensity.
    $maxBrightness = 0
    for($fgColour = 0; $fgColour -lt $colours.Count; $fgColour++)
    {
        for($bgColour = 0; $bgColour -lt $colours.Count; $bgColour++)
        {
            for($ditherCharacter = 0
                $ditherCharacter -lt $dithering.Count; 
                $ditherCharacter++)
            {
                $apparentBrightness = `
                    $luminances[$fgColour] * $ditherFactor[$ditherCharacter] +
                    $luminances[$bgColour] *
                        (1 - $ditherFactor[$ditherCharacter])
                    
                if($apparentBrightness -gt $maxBrightness) 
                { 
                    $maxBrightness = $apparentBrightness 
                }
                    
                $apparentBrightnesses[$apparentBrightness] = `
                    "$fgColour$bgColour$ditherCharacter"
            }
       }
    }

    ## Finally, we normalize our computed intesities into a pallete of
    ## 0 to 255.  If a given intensity is 30% towards our maximum intensity,
    ## then it should be in the palette at 30% of index 255.
    $paletteIndex = 0
    foreach($key in ($apparentBrightnesses.Keys | sort))
    {
        $keyValue = $apparentBrightnesses[$key]
        do
        {
            $character = $dithering[[Int32]::Parse($keyValue[2])]
            $fgColour = $colours[[Int32]::Parse($keyValue[0])]
            $bgColour = $colours[[Int32]::Parse($keyValue[1])]
            
            $bufferCell = `
                new-object System.Management.Automation.Host.BufferCell `
                    $character,
                    $fgColour,
                    $bgColour,
                    "Complete"
                    
            $palette[$paletteIndex] = $bufferCell
            $paletteIndex++
        } while(($paletteIndex / 256) -lt ($key / $maxBrightness))
    }
}

## Dump the palette to the screen.
function displayPalette
{
    for($paletteIndex = 254; $paletteIndex -ge 0; $paletteIndex--)
    {
        $bufferCell = $palette[$paletteIndex]
        $fgColor = $bufferCell.ForegroundColor
        $bgColor = $bufferCell.BackgroundColor
        $character = $bufferCell.Character

        $host.UI.RawUI.ForegroundColor = $fgColor
        $host.UI.RawUI.BackgroundColor = $bgColor
        write-host -noNewLine $character
    }
    
    write-host
}

. 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.]
[Edit: Updated to work with PowerShell RTM and updated Invoke-Inline.ps1 script]

Comments [0] | | # 
 Tuesday, December 13, 2005
Wednesday, December 14, 2005 7:48:21 AM (Pacific Standard Time, UTC-08:00) ( )

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]

################################################################################ 
## 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.]

Comments [0] | | #