.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

  • 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).

    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.

    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.

    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.

    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: