PowerShell P/Invoke Walkthrough

Mon, Jan 19, 2009 5-minute read

In version 1 of PowerShell, it was possible to access Win32 APIs in one of two ways: by generating a dynamic assembly on the fly (you wouldn’t really do this for one-off calls, but would probably do it in a script that makes it easier to invoke Win32 APIs,) or by looking up the P/Invoke definition for that API call, and compiling in-line C# to access it.

These are both good approaches, but we wanted to use the Add-Type cmdlet to really nail the language interop scenarios for PowerShell V2. Add-Type offers four basic modes of operation:

PS:13 > Get-Command Add-Type | Select -Expand ParameterSets | Select Name

Name
----
FromSource
FromMember
FromPath
FromAssemblyName

These are:

  • FromSource: Compile some C# (or other language) code that completely defines a type. This is useful when you want to define an entire class, its methods, namespace, etc. You supply the actual code as the value to the –TypeDefinition parameter, usually through a variable.
  • FromPath: Compile from a file on disk, or load the types from an assembly at that location.
  • FromAssemblyName: Load an assembly from the GAC by its shorter name. This is is not the same as [Reflection.Assembly]::LoadWithPartialName, since that introduces your script to many subtle breaking changes. Instead, PowerShell maintains a large mapping table that converts the shorter name you type a strongly-named assembly reference.
  • FromMember: Generates a type out of a member definition (or set of them.) For example, if you specify only a method definition, PowerShell automatically generates the wrapper class for you. This parameter set is explicitly designed to easily support P/Invoke calls.

Now, how do you use the FromMember parameter set to call a Win32 API? Let’s see. First, imagine that you want to access sections of an INI file. This previous blog post gives an example for V1.

PowerShell doesn’t have a native way to manage INI files, and neither does the .NET Framework. However, the Win32 API does, through a call to GetPrivateProfileString. The .NET framework lets you access Win32 functions through a technique called P/Invoke (Platform Invocation Services.) Most calls boil down to a simple “P/Invoke definition,” which usually takes a lot of trial and error. However, a great community has grown around these definitions, resulting in an enormous resource called P/Invoke .NET. The P/Invoke Interop Assistant is an awesome tool from the CLR team that also generates these definitions.

First, we’ll create a script, Get-PrivateProfileString.ps1. It’s a template for now:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$null

So first, we visit P/Invoke .NET and search for GetPrivateProfileString:

image

Click into the definition, and we see the C# signature:

image

Copy that signature as a here-string into our script. Notice that we’ve added public to the declaration. The signatures on PInvoke.NET assume that you’ll call the method from within the C# class that defines it. We’ll be calling it from scripts, so we need to change its visibility.

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$null

Now, we add the call to Add-Type. This signature becomes the building block for a new class, so we only need to give it a name. To prevent its name from colliding with other classes with the same name, we also put it in a namespace. The name of our script is a good choice:

## Get-PrivateProfileString.ps1
param(

    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -PassThru

$null

When we try to run this script, though, we get an error:

The type or namespace name 'StringBuilder' could not be found (are you missing a
using directive or an assembly reference?)
c:\Temp\obozeqo1.0.cs(12) :    string lpDefault,
c:\Temp\obozeqo1.0.cs(13) : >>>    StringBuilder lpReturnedString,
c:\Temp\obozeqo1.0.cs(14) :    uint nSize,

Indeed we are. StringBuilder is defined in the System.Text namespace, but the using directive goes at the top of the program by the class definition. Since we’re letting PowerShell define the type for us, we can either rename it to System.Text.StringBuilder, or add a –UsingNamespace parameter. (Aside: PowerShell adds references to the System and System.Runtime.InteropServices namespaces by default.) Let’s do the latter:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru

$null

Now, we can plug in all of the necessary parameters. The GetPrivateProfileString puts its output in a StringBuilder, so we’ll have to feed it one, and return its contents:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru
   
$builder = New-Object System.Text.StringBuilder 1024
$type::GetPrivateProfileString($category,
    $key, "", $builder, $builder.Capacity, $file)
   
$builder.ToString()

So now we have it. With just a few lines of code, we’ve defined and invoked a Win32 API call.

[C:\Users\leeholm]
PS:1 > Get-PrivateProfileString c:\windows\system32\tcpmon.ini "<Generic Network Card>" Name
Generic Network Card