en-US/about_CtypesStruct.help.txt

TOPIC
    about_ctypesstruct
 
SHORT DESCRIPTION
    It is very common for a C based API to use structs as arguments when dealing
    with complex data but there is no easy way to define a custom struct in
    PowerShell without restoring to `Add-Type`. This modules provides a helper
    function to define these structs for you.
 
LONG DESCRIPTION
    PowerShell currently supports defining classes using the `class` keyword but
    it has no way to define a struct type as well as the layout/field
    information that might be needed when using that struct as an argument for a
    PInvoke member. The `ctypes_struct` function supplied by this module is an
    alias for
    New-Ctypes-Struct
    and is designed to give a similar interface as the `class` keyword in
    PowerShell but for defining structs.
    The
    SECURITY_ATTRIBUTES struct
    is a simple struct used in various Windows PInvoke functions and its
    definition is:
 
    typedef struct _SECURITY_ATTRIBUTES {
      DWORD nLength;
      LPVOID lpSecurityDescriptor;
      BOOL bInheritHandle;
    } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
 
    There are 3 fields in this struct, an int, pointer, and boolean which can
    easily be represented in dotnet. To define this struct using
    `ctypes_struct`, take the fields that are defined and write it inside the
    scriptblock with the syntax `[FieldType]$FieldName` like so:
 
    ctypes_struct SECURITY_ATTRIBUTES {
        [int]$Length
        [IntPtr]$SecurityDescriptor
        [bool]$InheritHandle
    }
 
    By default if no type is specified, the type used is `[IntPtr]`. Once
    defined the struct can be created just like any other type in PowerShell:
 
    $sa = [SECURITY_ATTRIBUTES]::new()
    $sa.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($sa)
 
    It can also be defined by "casting" a hashtable with the field names and
    values in an easier short hand:
 
    $sa = [SECURITY_ATTRIBUTES]@{
        Length = [System.Runtime.InteropServices.Marshal]::SizeOf([type][SECURITY_ATTRIBUTES])
    }
 
    The scriptblock used in `ctypes_struct` is designed to be very simple and
    only allow lines that contain `[type]$Name` with an optional `[MarshalAs]`
    or `[FieldOffset]` attribute before the type. The scriptblock isn't invoked
    in any way, it is parsed at runtime to extract the lines and build the
    struct based on what was found. It is possible to define a struct with the
    same name, the caveat being the original definition can no longer be
    referenced by `[STRUCT_TYPE]` anymore even if it remains loaded. Only the
    last defined struct of the same name will be referred to by that name.
 
STRUCTLAYOUT ATTRIBUTE
    Typically when defining a struct for use in a PInvoke function, the
    StructLayoutAttribute
    is used to tell dotnet how to marshal the struct value when passing by
    reference and calculating the size. By default `ctypes_struct` applies the
    attribute `[StructLayout(LayoutKind.Sequential)]` to the struct it defines.
    This can be controlled using the following parameters:
    * `-CharSet`
    * `-LayoutKind`
    * `-Pack`
    These attributes corresponds to the same fields in the `StructLayout`
    attribute. For example, to define a struct with the Explicit layout, charset
    of Unicode, and a pack size of 8, the following is done:
 
    ctypes_struct -CharSet Unicode -LayoutKind Explicit -Pack 8 {
        ...
    }
 
FIELD ATTRIBUTES
    Each field in the struct can be defined with 2 different attributes:
*
    MarshalAsAttribute
    *
    FieldOffsetAttribute
    To define a field with one of these attributes, just place it before the
    type definition:
 
    ctypes_struct FIELD_MARSHAL_AS {
        [MarshalAs('LPWStr')][string]$StringField
    }
     
    ctypes_struct FIELD_OFFSET_STRUCT -LayoutKind Explicit {
        [FieldOffset(0)][int]$IntFieldAt0
        [FieldOffset(4)][int]$IntFieldAt4
    }
 
    The `FieldOffset` is a simple attribute that only accepts an integer value
    being the field offset to use. The `MarshalAs` attribute accepts either the
    UnmanagedType
    enum name as a string or an integer representing the unmanaged type value.
    It also accepts the `ArraySubType` and `SizeConst` values with
    `ArraySubType` being the `UnmanagedType` string/int value and `SizeConst`
    being the int representing the number of elements in the array field value.
    Here is a more complex `MarshalAs` field example with those optional values:
 
    ctypes_struct LUID {
        [int]$LowPart
        [int]$HighPart
    }
     
    ctypes_struct LUID_AND_ATTRIBUTES {
        [LUID]$Luid
        [int]$Attributes
    }
     
    ctypes_struct TOKEN_PRIVILEGES {
        [int]$PrivilegeCount
        [MarshalAs('ByValArray', SizeConst=1)][LUID_AND_ATTRIBUTES[]]$Privileges
    }
 
    Note: Due to limitations in the scriptblock validator, the unmanaged type
    value must be a string constant or int, it cannot be the enum::value.
 
USING POINTER TO A STRUCT
    While some functions accept the struct value itself being passed by
    reference, many functions require a pointer to a struct. For example the
    CreateProcessW
    and required structures have the following signatures:
 
    BOOL CreateProcessW(
      [in, optional] LPCWSTR lpApplicationName,
      [in, out, optional] LPWSTR lpCommandLine,
      [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
      [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
      [in] BOOL bInheritHandles,
      [in] DWORD dwCreationFlags,
      [in, optional] LPVOID lpEnvironment,
      [in, optional] LPCWSTR lpCurrentDirectory,
      [in] LPSTARTUPINFOW lpStartupInfo,
      [out] LPPROCESS_INFORMATION lpProcessInformation
    );
     
    typedef struct _SECURITY_ATTRIBUTES {
      DWORD nLength;
      LPVOID lpSecurityDescriptor;
      BOOL bInheritHandle;
    } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
     
    typedef struct _STARTUPINFOW {
      DWORD cb;
      LPWSTR lpReserved;
      LPWSTR lpDesktop;
      LPWSTR lpTitle;
      DWORD dwX;
      DWORD dwY;
      DWORD dwXSize;
      DWORD dwYSize;
      DWORD dwXCountChars;
      DWORD dwYCountChars;
      DWORD dwFillAttribute;
      DWORD dwFlags;
      WORD wShowWindow;
      WORD cbReserved2;
      LPBYTE lpReserved2;
      HANDLE hStdInput;
      HANDLE hStdOutput;
      HANDLE hStdError;
    } STARTUPINFOW, *LPSTARTUPINFOW;
     
    typedef struct _PROCESS_INFORMATION {
      HANDLE hProcess;
      HANDLE hThread;
      DWORD dwProcessId;
      DWORD dwThreadId;
    } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
 
    The `lpProcessAttributes` and `lpThreadAttributes` accept an
    `LPSECURITY_ATTRIBUTES` which in the structs definition is defined as a
    pointer `SECURITY_ATTRIBUTES,
    PSECURITY_ATTRIBUTES,
    LPSECURITY_ATTRIBUTES;`. The same applies to `lpStartupInfo` and
    `lpProcessInformation` being pointers to the structs themselves. This means
    that instead of passing the struct by value, they must be passed by
    reference. To pass a struct by reference, simply use the `[ref]` attribute
    on the variable when calling the function, or on the type when declaring the
    function explicitly. Here is a full example of calling `CreateProcess` using
    this module:
 
    ctypes_struct SECURITY_ATTRIBUTES {
        [int]$Length
        [IntPtr]$SecurityDescriptor
        [bool]$InheritHandle
    }
     
    ctypes_struct STARTUPINFOW -CharSet Unicode {
        [int]$CB
        [MarshalAs('LPWStr')][string]$Reserved
        [MarshalAs('LPWStr')][string]$Desktop
        [MarshalAs('LPWStr')][string]$Title
        [int]$X
        [int]$Y
        [int]$XSize
        [int]$YSize
        [int]$XCountChars
        [int]$YCountChars
        [int]$FillAttribute
        [int]$Flags
        [short]$ShowWindow
        [short]$Reserved2
        [IntPtr]$Reserved3
        [IntPtr]$StdInput
        [IntPtr]$StdOutput
        [IntPtr]$StdError
    }
     
    ctypes_struct PROCESS_INFORMATION -LayoutKind Sequential {
        [IntPtr]$Process
        [IntPtr]$Thread
        [int]$Pid
        [int]$Tid
    }
     
    # There are more options but limited here for the sake of brevity
    [Flags()] enum ProcessCreationFlags {
        NONE = 0x00000000
        CREATE_NEW_CONSOLE = 0x00000010
        CREATE_UNICODE_ENVIRONMENT = 0x00000400
    }
     
    $kernel32 = New-CtypesLib Kernel32.dll
     
    # This step is optional and am example of how to define a pointer to struct
    $kernel32.Returns([bool]).SetLastError().CharSet('Unicode').CreateProcessW = [Ordered]@{
        # LPCWSTR is a constant, can use the normal string type
        lpApplicationName = [string]
     
        # LPWSTR is not constant, need to use StringBuilder for this as the
        # function can mutate the string which isn't allowed by [string]
        lpCommandLine = [System.Text.StringBuilder]
     
        # LPSECURITY_ATTRIBUTES is a pointer to SECURITY_ATTRIBUTES, use [ref]
        lpProcessAttributes = [ref][SECURITY_ATTRIBUTES]
     
        # Using [IntPtr] is also allowed as that allows the caller to do
        # [IntPtr]::Zero to specify NULL
        lpThreadAttributes = [IntPtr]
     
        bInheritHandles = [bool]
     
        # Can also be [int] but defining an enum gives completion support as well
        # as specify the value as a string. WinPS (5.1) must use [int] here instead
        # as it cannot reference an enum type defined in PowerShell.
        dwCreationFlags = [ProcessCreationFlags]
     
        # LPVOID is the same as IntPtr
        lpEnvironment = [IntPtr]
     
        lpCurrentDirectory = [string]
     
        lpStartupInfo = [ref][STARTUPINFOW]
     
        lpProcessInformation = [ref][PROCESS_INFORMATION]
    }
     
    $procSa = [SECURITY_ATTRIBUTES]@{
        Length = [System.Runtime.InteropServices.Marshal]::SizeOf([Type][SECURITY_ATTRIBUTES])
        InheritHandle = $true
    }
    $si = [STARTUPINFOW]@{
        CB = [System.Runtime.InteropServices.Marshal]::SizeOf([Type][STARTUPINFOW])
    }
    $pi = [PROCESS_INFORMATION]::new()
     
    $commandLine = [System.Text.StringBuilder]::new("powershell.exe -NoExit -Command 'hi'")
     
    # The Returning, SetLastError, and CharSet is not needed if explicitly defined above
    $res = $kernel32.Returns([bool]).SetLastError().CharSet('Unicode').CreateProcessW(
        "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
        $commandLine,
        [ref]$procSa,
        [IntPtr]::Zero,
        $false,
        [ProcessCreationFlags]'CREATE_NEW_CONSOLE, CREATE_UNICODE_ENVIRONMENT',
        [IntPtr]::Zero,
        "C:\Windows",
        [ref]$si,
        [ref]$pi
    )
    if (-not $res) {
        throw [System.ComponentModel.Win32Exception]$kernel32.LastError
    }
     
    $kernel32.CloseHandle($pi.Process)
    $kernel32.CloseHandle($pi.Thread)
 
    Two major things to point out here
    * Windows PowerShell 5.1 cannot reference an enum defined in PowerShell
    using the `enum` syntax
    * PowerShell 6+ copies the PowerShell enum when declared into the struct assembly
    A consequence of the second point is that when you declare a field with an
    enum defined in PowerShell in a `ctypes_struct`, the enum will now persist
    globally rather than be collected when it goes out of scope.