PowerShell 2.0 CTP3: Modules, in Practice - Closures

After some hints from Ibrahim on the Microsoft PowerShell team, I realised it was possible to rewrite the dynamic module body generation from the Get-Interface function in my last post without using string literals and nested here-string tricks. I was able to increase the brevity by using a new [to PowerShell] v2 feature called “Closures.” Ibrahim talks a bit about the technique over on the official PowerShell Blog. It’s a common feature in so-called Functional languages and there are plenty of other tidbits to read about it, both academic and practical if you search online.

A closure in PowerShell, in short, lets you take a snapshot of a ScriptBlock by calling its GetNewClosure() method. The snapshot is taken of its variables and will let you pass it around so it is no longer directly tied to the particular scope chain it was declared in. As some are fond of saying though, Thar Be Dragons. Although closing around a ScriptBlock will make copies of the PSVariables that are within, it is still subject to the whims of .NET – particularly ByRef and ByValue semantics. If a ScriptBlock contains a PSVariable pointing to an integer, it’s safe to say that integer will be copied and is frozen as that value as long as you remain within the domain of the current SessionState context (more about this later).

.NET intervenes: ByRef and ByVal

Closing around a ScriptBlock that contains PSVariables that point to reference types, however - like Arrays for example – will only duplicate the reference, not the instance itself. If elements in the array are changed via the PSVariable from the originating scope, the corresponding array element in the closure will also change. After all – you got a copy of the pointer, not the value itself. This is the very essence of ByRef and ByVal.

Let’s take a look at the revised Get-Interface script:

function Get-Interface {

#.Synopsis
#   Allows PowerShell to call specific interface implementations on any .NET object. 
#.Description
#   Allows PowerShell to call specific interface implementations on any .NET object. 
#
#   As of v2.0, PowerShell cannot cast .NET instances to a particular interface. This makes it
#   impossible (shy of reflection) to call methods on explicitly implemented interfaces.   
#.Parameter Object
#   An instance of a .NET class from which you want to get a reference to a particular interface it defines.
#.Parameter InterfaceType
#   An interface type, e.g. [idisposable], implemented by the target object.
#.Example
#   // a class with explicitly implemented interface   
#   public class MyObj : IDisposable {
#      void IDisposable.Dispose()
#   }
#   
#   ps> $o = new-object MyObj
#   ps> $i = get-interface $o ([idisposable])
#   ps> $i.Dispose()      
#.ReturnValue
#   A PSCustomObject with ScriptMethods and ScriptProperties representing methods and properties on the target interface.
#.Notes
# AUTHOR:    Oisin Grehan http://www.nivot.org/
# LASTEDIT:  2009-03-28 18:37:23
# REVISION:  0.2

    [CmdletBinding()]
    param(
        [ValidateNotNull()]
        $Object,
        
        [ValidateScript( { $_.IsInterface } )]
        [type]$InterfaceType
    )

    $script:t  = $Object.GetType()
    
    try {
        
        $script:m  = $t.GetInterfaceMap($InterfaceType)

    } catch [argumentexception] {
        
        throw "Interface $($InterfaceType.Name) not found on ${t}!"
    }

    $script:im = $m.InterfaceMethods
    $script:tm = $m.TargetMethods
    
    # TODO: use param blocks in functions instead of $args
    #       so method signatures are visible via get-member
    
    $body = {
         param($o, $i) 
         
         $script:t  = $o.GetType()
         $script:m  = $t.GetInterfaceMap($i)
         $script:im = $m.InterfaceMethods
         $script:tm = $m.TargetMethods
                  
         for ($ix = 0; $ix -lt $im.Count; $ix++) {
            
            $mb = $im[$ix]

            # for the function body, we close over $ix to capture the index
            # so even on the next iteration of this loop, the $ix value will
            # be frozen within the function's scriptblock body
            set-item -path function:script:$($mb.Name) -value {

                # call corresponding target method
                $tm[$ix].Invoke($o, $args)

            }.GetNewClosure() -verbose -force

            if (!$mb.IsSpecialName) {
                # only export the function if it is not a getter or setter.
                Export-ModuleMember $mb.Name -verbose
            }
         }
    }

    write-verbose $body.tostring()    
    
    # create dynamic module
    $module = new-module -ScriptBlock $body -Args $Object, $InterfaceType -Verbose
    
    # generate method proxies - all exported members become scriptmethods
    # however, we are careful not to export getters and setters.
    $custom = $module.AsCustomObject()
    
    # add property proxies - need to use scriptproperties here.
    # modules cannot expose true properties, only variables and 
    # we cannot intercept variables get/set.
    
    $InterfaceType.GetProperties() | % {

        $propName = $_.Name
        $getter   = $null
        $setter   = $null
       
        if ($_.CanRead) {
            
            # where is the getter methodinfo on the interface map?
            $ix = [array]::indexof($im, $_.GetGetMethod())
            
            # bind the getter scriptblock to our module's scope
            # and generate script to call target method
            #
            # NOTE: we cannot use a closure here because sessionstate
            #       is rebound to the module's, and $ix would be lost
            $getter = $module.NewBoundScriptBlock(
                [scriptblock]::create("`$tm[{0}].Invoke(`$o, @())" -f $ix))
        }
        
        if ($_.CanWrite) {
            
            # where is the setter methodinfo on the interface map?
            $ix = [array]::indexof($im, $_.GetSetMethod())
            
            # bind the setter scriptblock to our module's scope
            # and generate script to call target method
            #
            # NOTE: we cannot use a closure here because sessionstate
            #       is rebound to the module's, and $ix would be lost
            $setter = $module.NewBoundScriptBlock(
                [scriptblock]::create(
                    "param(`$value); `$tm[{0}].Invoke(`$o, `$value)" -f $ix))
        }
        
        # add our property to the pscustomobject
        $prop = new-object management.automation.psscriptproperty $propName, $getter, $setter
        $custom.psobject.properties.add($prop)
    }
    
    # insert the interface name at the head of the typename chain (for get-member info)
    $custom.psobject.TypeNames.Insert(0, $InterfaceType.FullName)
    
    # dump our pscustomobject to pipeline
    $custom
}

The changes are around line 56, where I now declare $body with a script syntax, no longer using [scriptblock]::create. Where previously I used a subexpression inside a here-string to general literal function declarations, I now use a “for” loop and set-item to create the functions (thanks Ibrahim!). You can see that I am calling GetNewClosure() on the function body each time I create a new function. This “captures” the value of the $ix index variable, so that the value will not change on subsequent loops. If I did not use closures, when the for loop ended, all of the functions would be using the same terminating value of the loop for $ix, which would be $im.count + 1. Doh.

Module SessionState Context, Wha?

So if I ripped out that literal string parsing stuff for the module $body, why am I still using it for the property getter/setter generation? (lines 115 / 130)  Well, I have no choice: Creating a new module creates an entirely new SessionState; in simple terms, session state is essentially a giant Hashtable in which PSVariables are stored. If I closed over those get/set ScriptBlocks containing the $ix PSVariable, when it is bound to the module it will use the new module’s SessionState – which is empty! The value of $ix would be undefined. The other variables in those strings are escaped, because they will be evaluated in the module’s context later, when the getters and setters are invoked. Those variables are seeded by the module’s initialization done at the new-module call at line 88.

It’s all very succinct, but rest assured, very powerful.

Have fun!

NOTE: I realise that some more refactoring could probably eliminate this script parsing, but the value is in understanding how modules may affect usage of closures, and the problems that may occur.

blog comments powered by Disqus

About the author

Irish, PowerShell MVP, .NET/ASP.NET/SharePoint Developer, Budding Architect. Developer. Montrealer. Opinionated. Montreal, Quebec.

Month List

Page List