Search code examples
.netpowershelltypesadd-type

Add-Type variations: -MemberType vs. -TypeDefinition parameters


Can someone explain the difference between this approach to Add-Type

$definition = [Text.StringBuilder]"" 
    [void]$definition.AppendLine('[DllImport("user32.dll")]') 
    [void]$definition.AppendLine('public static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer);') 
    [void]$definition.AppendLine('[DllImport("kernel32.dll")]') 
    [void]$definition.AppendLine('public static extern IntPtr LoadLibrary(string s);') 
    Add-Type -memberDefinition:$definition.ToString() -name:Utility -namespace:PxTools

And something like this

Add-Type -typeDefinition @"
public class BasicTest
{
  public static int Add(int a, int b)
    {
        return (a + b);
    }
  public int Multiply(int a, int b)
    {
    return (a * b);
    }
}
"@

I see examples of the latter quite often, but the former I have only ever seen in some sample code to Pin to Taskbar. Are these just two different ways to skin a cat, or is the former required in some use cases? And, if both are valid all the time, what would it look like to use the latter method with the code in the former?

EDIT: I considered making this a new thread, but it seems to me this is an expansion on the original question, so hopefully this is the correct approach.

I have implemented code based on what I learned from this post...

$targetFile = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Snipping Tool.lnk"
$action = 'PinToTaskbar'

$verbs = @{  
    'PinToStartMenu' = 5381 
    'UnpinFromStartMenu' = 5382 
    'PinToTaskbar' = 5386 
    'UnpinFromTaskbar' = 5387
}

try { 
    $type = [type]"PxTools.Utility" 
}  catch { 
    $definition = [Text.StringBuilder]"" 
    [void]$definition.AppendLine('[DllImport("user32.dll")]') 
    [void]$definition.AppendLine('public static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer);') 
    [void]$definition.AppendLine('[DllImport("kernel32.dll")]') 
    [void]$definition.AppendLine('public static extern IntPtr LoadLibrary(string s);') 
    Add-Type -memberDefinition:$definition.ToString() -name:Utility -namespace:PxTools          
} 
if ($script:Shell32 -eq $null) {         
    $script:Shell32 = [PxTools.Utility]::LoadLibrary("shell32.dll") 
} 
$maxVerbLength = 255 
$verb = new-object Text.StringBuilder "", $maxVerbLength 
[void][PxTools.Utility]::LoadString($script:Shell32, $verbs.$action, $verb, $maxVerbLength) 
$verbAsString = $verb.ToString()
try {
    $path = Split-Path $targetFile -parent -errorAction:stop
    $file = Split-Path $targetFile -leaf -errorAction:stop
    $shell = New-Object -com:"Shell.Application" -errorAction:stop 
    $folder = $shell.Namespace($path)    
    $target = $($folder.Parsename($file)).Verbs() | Where-Object {$_.Name -eq $verbAsString}
    $target.DoIt()
    Write-Host "$($action): $file"
} catch {
    Write-Host "Error managing shortcut"
}

Now I have three questions about refactoring this.

1: How would I refactor the Add-Type to use a Here-String? EDIT: This seems to work, so I'll revise the question to be, is this the best/most elegant solution, or could it be improved?

Add-Type -memberDefinition:@"
    [DllImport("user32.dll")]
    public static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer);
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string s);
"@  -name:Utility -namespace:PxTools

2: Test.StringBuilder is used both as the type of the $definition string and in the LoadString method. Is this required, or could this be implemented without using StringBuilder at all? EDIT: I eliminated SB as the data type in the refactor above, but not sure about doing it in LoadString. Probably because I am still trying to get my head around exactly what the code is doing, before I go changing it.

3: Does the $script:Shell32 bit need to be handled like this, or could it not be rolled into the Type instead? And, is the original Global scope (which I changed to Script scope) really necessary? Seems to me that unless I was doing this hundreds of times calling LoadLibrary multiple times wouldn't be that big a deal?


Solution

    • Re 1:

    Yes, using a here-string this way is the best approach.

    On a side note, using : to separate parameter names from their values works, but is unusual; typically, a space is used (e.g., -name Utility rather than -name:Utility).

    • Re 2:

    There is NO good reason to use [System.Text.StringBuilder] here in the type definition.
    Use a here-string, regular string, or a string array.
    Aside from use in a Windows API call, as you demonstrate, the only reason you'd ever consider using [System.Text.StringBuilder] in PowerShell is if performance is paramount and you need to build up a very large string from dynamically created pieces.

    Gordon himself notes that using [System.Text.StringBuilder] in the sb parameter of the LoadString() Windows API function is necessary, because it is an out parameter that receives a string, whereas the [string] type is immutable.

    • Re 3:

    It is possible to combine the two approaches - P/Invoke signatures [DllImport(... with -MemberType on the one hand, and a custom type definition (class BasicTest ...) with -TypeDefinition on the other (for background information on both approaches, see the bottom of this post).

    Side note re script scope: script is the default scope inside scripts, so at the top level of a script, all variables you create are implicitly script-scoped; thus, $script:Shell32 = ... is effectively the same as $Shell32 = ... in the top-level scope of a script. You can even reference that script-level variable from inside a function as just $Shell32 (though you may wish to use $script:Shell32 for clarity). The only time you need the $script: scope qualifiers is if you've created a local $Shell32 variable (e.g., implicitly, simply by assigning to $Shell32) that shadows the script-level one.

    • The code below is a refactoring that creates a single helper class with Add-Type -TypeDefinition into which the P/Invoke signatures are integrated (no need for a separate Add-Type -MemberDefinition call).

      • The helper type created has only one static method, GetVerbName(). Note that I've removed the public access modifier from the P/Invoke signatures to make them private, because they're now only needed class-internally.

      • The helper method loads and frees the "shell32.dll DLL on every call, but I don't expect that to be a problem performance wise.

      • You could expand this approach to move all your non-PowerShell code into this helper class (calling the Shell.Application COM object). If you did that, and implemented, say, a PinToTaskBar static method, you could simply reference [PxTools.TaskBarStartMenuHelper]::PinToTaskBar() from anywhere in your script, and needn't even worry about script-level variables.

    • I've replaced the $verbs hashtable with a cleaner Enum definition, but note that this only works in PSv5+.

    Enum TaskBarStartMenuVerbs {  
      PinToStartMenu = 5384
      UnpinFromStartMenu = 5385 
      PinToTaskbar = 5386
      UnpinFromTaskbar = 5387
    }
    
    Add-Type -TypeDefinition @'
    using System;
    using System.Runtime.InteropServices;
    using System.Text;
    
    namespace PxTools {    
      public class TaskBarStartMenuHelper {
        
        [DllImport("user32.dll")] 
        static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer); 
        [DllImport("kernel32.dll")] 
        static extern IntPtr LoadLibrary(string s);
        [DllImport("kernel32.dll")] 
        static extern bool FreeLibrary(IntPtr h);
        
        public static string GetVerbName(uint verbId) {
          IntPtr h = LoadLibrary("shell32.dll");
          const int maxLen = 255;
          var sb = new StringBuilder(maxLen);
          LoadString(h, verbId, sb, maxLen);
          FreeLibrary(h);
          return sb.ToString();
        }         
      }    
    }
    '@ 
    
    # This returns 'Pin to tas&kbar' on a US English system.
    [PxTools.TaskBarStartMenuHelper]::GetVerbName([TaskBarStartMenuVerbs]::PinToTaskbar)
    

    Also note that it's fine to call Add-Type repeatedly in a session, as long as the type definition hasn't changed. Subsequent invocations are effectively a fast and silent no-op.
    In other words: no need to explicitly check for a type's existence and define it conditionally. If a different type of the same name is already loaded, the Add-Type will fail, which, however is desirable, because you want to make sure that you're using the type you want.


    Background Information re -MemberDefinition

    Reference: Get-Help Add-Type.

    • The first approach - AddType -MemberDefinition ... -Name ... -NameSpace is typically used to pass a string containing public P/Invoke signatures that enable access to native code such as Windows API functions via a custom helper class that exposes them as static methods.

      • When executing the first command, type [PxTools.Util] is created with static methods [PxTools.Util]::LoadString() and [PxTools.Util]::LoadLibrary() that call the underlying Windows API functions.

      • While -MemberDefinition is typically used with P/Invoke signatures, it is not limited to that, because -MemberDefinition is just syntactic sugar for wrapping the specified string in a public class (see below for details). Thus, you can pass anything that is valid inside a class body, such as property and method definitions, as a way of defining your custom class more succinctly, without having to include explicit namespace and class blocks.
        Note, however, that it is invariably a class that is created, so you cannot use this approach to define a struct, for instance; also, you cannot use using statements directly; instead, pass the namespaces via the -UsingNamespace parameter.

      • Not specifying a -NameSpace value places the type in namespace Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes, meaning that you'll have to refer to it as [Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.<yourTypeName>] (note how that differs from a -TypeDefinition command without a namespace <name> { ... } enclosure - see below).

    • The second approach - Add-Type -TypeDefinition ... - is used to define a custom type (class) by way of a string containing C# source code (by default; VisualBasic and JScript are also supported).
      A custom type can be a class, delegate, enum, interface, or struct definition, typically enclosed in a namespace declaration.

      • When executing the 2nd command, type [BasicTest] is created (without a namespace qualification, because the type definition in the source-code string isn't enclosed in namespace <name> { ... }), with static method Add and instance method Multiply.

    In both cases members must be declared as public in order to be accessible from PowerShell.

    Note that the -MemberDefinition syntax is essentially just syntactic sugar in that it automatically provides a class wrapper around the P/Invoke signatures to facilitate access to native DLL calls in cases where you want to call them directly from PowerShell rather than use them internally in a custom type defined with Add-Type -TypeDefinition.

    Example:

    The following -MemberDefinition call:

    Add-Type -Namespace PxTools -Name Utility -MemberDefinition @'
        [DllImport("user32.dll")]
        public static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer);
        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string s);
        [DllImport("kernel32.dll")] 
        public static extern bool FreeLibrary(IntPtr h);
    '@  
    

    is syntactic sugar for the following -TypeDefinition call:

    Add-Type -TypeDefinition @'
    using System;
    using System.Runtime.InteropServices;
    
    namespace PxTools {
      public class Utility {
        [DllImport("user32.dll")]
        public static extern int LoadString(IntPtr h, uint id, System.Text.StringBuilder sb, int maxBuffer);
        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string s);
        [DllImport("kernel32.dll")] 
        public static extern bool FreeLibrary(IntPtr h);
      }  
    }
    '@  
    

    In other words: Add-Type -MemberDefinition:

    • automatically wraps the public static methods defined in the source code in a public class with the specified name (-Name) in the specified namespace (-NameSpace), so that these methods can be called as static methods on that class, as [<NameSpace>.<Name>]::<Method>(...)

      • While this is typically used to expose P/Invoke signatures - calls to unmanaged functions, notably Windows API functions - it is not limited to that, and you can also use the technique to generally define general-purpose helper classes with (public) static utility methods containing regular (managed) C# code. Note that you cannot use using statements directly; instead, pass the namespaces via the -UsingNamespace parameter.
    • implicitly prepends the necessary using statements needed to support compilation of the P/Invoke signatures.