en-US/about_CtypesInterface.help.txt
TOPIC
about_ctypesinterface SHORT DESCRIPTION The ctypes library object returned by New-CtypesLib is a special object that is designed to dynamically build the PInvoke extern method when requested based on the arguments provided. It does this through reflection and analysing the existing values provided before building the `PSCodeMethod` which ultimately calls that newly creating PInvoke method. LONG DESCRIPTION There are 2 ways of defining a PInvoke method to call in a library: * Calling the method directly with the arguments desired * Explicitly defining a new property with an array or ordered dict defining the types needed for the method The OpenProcess function has the following signature. HANDLE OpenProcess( [in] DWORD dwDesiredAccess, [in] BOOL bInheritHandle, [in] DWORD dwProcessId ); Invoking it directly without pre-defining the signature is possible by using variables with the same types: $kernel32 = New-CtypesLib kernel32.dll $kernel32.OpenProcess[IntPtr](0x400, $false, 1234) _Note: The return type was defined using the new generics syntax added in PowerShell 7.3. Older PowerShell versions should use the `.Returns([Type])` method instead. Once called, the signature for this function is stored on the `$kernel32` object and can be viewed with `Get-Member`. Any other functions called will also appear here. To predefine the function before it is called, simply assign an array or ordered dictionary with the parameter types needed. For example using an array looks like $kernel32.Returns([IntPtr]).OpenProcess = @( [int], [bool], [int] ) An ordered dictionary can also be used to give a label to the arguments: $kernel32.Returns([IntPtr]).OpenProcess = [Ordered]@{ DesiredAccess = [int] InheritHandle = [bool] ProcessId = [int] } As shown on the method definition, these field names appear in the overload defition rather than a generic arg name $kernel32 | Get-Member -Name OpenProcess TypeName: Ctypes.Library Name MemberType Definition ---- ---------- ---------- OpenProcess CodeMethod static System.IntPtr OpenProcess(psobject lib, int DesiredAccess, bool InheritHandle, int ProcessId) It is also possible to use `$null` as a representation for `[IntPtr]::Zero`. If `$null` was used as a type value when defining a signature, or used as an argument for a method without a predefined signature, the argument type is `IntPtr` and the value becomes `IntPtr.Zero`. This makes it possible to use `$null` as a shorthand for `[IntPtr]::Zero`. Take CloseHandle as an example. BOOL CloseHandle( [in] HANDLE hObject ); This can be used like $lib.Returns([bool]).CloseHandle($null) # Both of these are the same when pre-defining a function $lib.Returns([bool]).CloseHandle = @($null) $lib.Returns([bool]).CloseHandle = @([IntPtr]) Predefining the argument types is useful because we can take advantage of PowerShell's casting behaviour to ensure the correct type is used rather than what is the value is when called. For example, using an explicit enum type allows the caller to use the enum string name rather than the enum or integer value itself. It is important to note that once a method has been defined or called on a library object, the signature cannot be changed. Trying to use a new return type or any one of the special attributes documented below will only work when the method is explicitly defined for the first time or, if not explicitly defined, when it is first called. To use a new method signature, simply create a new library object in another variable and try again. RETURN TYPES By default all PInvoke functions called have a return type of `[int]`. If the function does not normally return an int this value may have no meaning or be unusable. There are two ways to define an explicit return type for a PInvoke function. The first is universal and works across all PowerShell versions and when pre-defining the method signature through the `Returns` function. $lib.Returns([IntPtr]).MyFunction($true) # or $lib.Returns([IntPtr]).MyFunction = @([bool]) This will call the `MyFunction` PInvoke method on the native library `$lib` represents and defines the return value as an `IntPtr`. The alternative way is to use the new PowerShell generics featured shipped in PowerShell 7.3. This only works when calling the function directly for the first time without it being pre-defined. # PowerShell 7.3+ only $lib.MyFunction[IntPtr]($true) Once a function has been defined or called once, it is not possible to change the return type. Using `Returns` or the generic type invocation after it has been defined will not change the return type and is effectively a no-op. To silence any output using the `[void]` type like so: $lib.Returns([void]).MyFunction($true) # or $lib.MyFunction[void]($true) REFERENCE TYPES It is common to encounter a PInvoke method that uses a pointer as an argument. One way is to pass through an `[IntPtr]` typed value that points to the value but another option is to pass the value by reference. In PowerShell this is achieved by using the `[ref]` attribute. For example the function OpenProcessToken has the following signature BOOL OpenProcessToken( [in] HANDLE ProcessHandle, [in] DWORD DesiredAccess, [out] PHANDLE TokenHandle ); The `TokenHandle` argument uses a `PHANDLE` type which is a pointer to a `HANDLE` which in C# is a pointer to an `IntPtr`. Pre-defining this signature would look like $advapi32 = New-CtypesLib Advapi32.dll $advapi32.Returns([bool]).OpenProcessToken = [Ordered]@{ ProcessHandle = [IntPtr] DesiredAccess = [System.Security.Principal.TokenAccessLevels] TokenHandle = [ref][IntPtr] } Calling this method would look like $token = [IntPtr]::Zero $advapi32.OpenProcessToken( $procHandle, 'Query', [ref]$token) Note: The definition of this function is not strictly required, if a function is not defined but invoked directory, a reference type is still used when the argument was passed with `[ref]`. DLLIMPORT ATTRIBUTES When defining a PInvoke function in C# code the DllImportAttribute is used to define the metadata needed for C# to hook into native function. This attribute can be used to define the following: * The dll/lib name * The calling convention * The character set used for string marshaling * The function name to be called in the dll/lib * Whether to store the native last error code value after calling It is possible to also set the above using the Ctypes library object created by `New-CtypesLib` using the following methods on that object: |DllImport Value|Ctypes Lib Method|Default| |-|-|-| |Dll/Lib Name|Part of `New-CtypesLib LibNamed.dll`|Mandatory| |CallingConvention|`$lib.CallingConvetention($cc)`|Winapi (StdCall on Windows and Cdecl on Linux)| |CharSet|`$lib.CharSet($cs)`|Auto| |EntryPoint|`$lib.EntryPoint($e)`|Uses the method name| |SetLastError|`$lib.SetLastError()`|False| All the functions return the same library object so can be chained together. For example to set the char set and enable last error caching the following can be used: $res = $lib.CharSet('Unicode').SetLastError().CreateFileW[IntPtr]( "C:\temp\test.txt", 0, [System.IO.FileShare]::Read, [IntPtr]::Zero, [System.IO.FileMode]::Create, 0, [IntPtr]::Zero) if ($res -eq [IntPtr](-1)) { throw [System.ComponentModel.Win32Exception]$lib.LastError } The character set will use `[System.Runtime.InteropServices.CharSet]::Unicode`, store the last error code set by the function and return the value as an `IntPtr`. On a failure it retrieves the error code through the `LastError` property on the object. Do not use `[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()` as that value could have been mutated by the PowerShell engine between statements. The `LastError` property is retrieved using that code but at a stage where PowerShell may not have overidden it. MARSHALAS ATTRIBUTES Another attribute used in PInvoke definitions is the ability to mark specific arguments with a MarshalAsAttribute . This attribute can be used to control how strings are marshaled as, or define array marshaling behaviour. To mark an argument with this attribute, use the `$lib.MarshalAs($obj, $unmanagedType)` function. This function adds a hidden PSNoteProperty to the object and is applied to the PInvoke definition when it's dynamically created for the first time. Note: Just like any other method metadata value, this only applies when the method is first defined. If used on a method already defined, it will be silently ignored. Using the `CreateFileW` example above, here is how to mark the first argument with `[MarshalAs(UnmanagedType.LPWStr)]`: $lib.Returns([IntPtr]).SetLastError().CreateFileW = @( $lib.MarshalAs([string], "LPWStr"), [int], [System.IO.FileShare], [IntPtr], [System.IO.FileMode], [int], [IntPtr] ) # Or with named arguments $lib.Returns([IntPtr]).SetLastError().CreateFileW = [Ordered]@{ lpFileName = $lib.MarshalAs([string], "LPWStr") dwDesiredAccess = [int] dwShareMode = [System.IO.FileShare] lpSecurityAttributes = [IntPtr] dwCreationDisposition = [System.IO.FileMode] dwFlagsAndAttributes = [int] hTemplateFile = [IntPtr] } # Or without an explicit signature $lib.SetLastError().Returns([IntPtr]).CreateFileW( $lib.MarshalAs("C:\temp\test.txt", "LPWStr"), 0, [System.IO.FileShare]::Read, [IntPtr]::Zero, [System.IO.FileMode]::Create, 0, [IntPtr]::Zero) DELEGATE FUNCTIONS Some C APIs require an argument that is a delegate/function called inside the unmanaged function. A delegate or callback argument can be written as a PowerShell scriptblock and the Ctypes code will automatically infer the return type and argument types from the scriptblock definition. To define a proper delegate it is important to define both the `[OutputType()]` attribute and the `[Type]` of each parameter in a `param()` block. For example the CertEnumSystemStoreLocation function is defined as: BOOL CertEnumSystemStoreLocation( [in] DWORD dwFlags, [in] void *pvArg, [in] PFN_CERT_ENUM_SYSTEM_STORE_LOCATION pfnEnum ); PFN_CERT_ENUM_SYSTEM_STORE_LOCATION PfnCertEnumSystemStoreLocation; BOOL PfnCertEnumSystemStoreLocation( [in] LPCWSTR pwszStoreLocation, [in] DWORD dwFlags, [in] void *pvReserved, [in] void *pvArg ) {...} This is how you can call this function using a scriptblock: $crypt = New-CtypesLib Crypt32.dll $stores = [System.Collections.Generic.List[string]]::new() $storeHandle = [System.Runtime.InteropServices.GCHandle]::Alloc($stores) $res = $crypt.Returns([bool]).SetLastError().CertEnumSystemStoreLocation( 0, [System.Runtime.InteropServices.GCHandle]::ToIntPtr($storeHandle), { [OutputType([bool])] param ( [System.Runtime.InteropServices.MarshalAs([System.Runtime.InteropServices.UnmanagedType]::LPWStr)] [string]$StoreLocation, [int]$Flags, [IntPtr]$Reserved, [IntPtr]$Arg ) $myListPtr = [System.Runtime.InteropServices.GCHandle]::FromIntPtr($Arg) $myList = [System.Collections.Generic.List[String]]($myListPtr.Target) $myList.Add($StoreLocation) return $true }) $storeHandle.Free() if (-not $res) { throw [System.ComponentModel.Win32Exception]$crypt.LastError } $stores The `[OutputType([bool])]` is used to denote the delegate return the type `bool` as per the signature. The `param` block defines each of the delegate arguments and their types. The `pwszStoreLocation` is defined as a `[String]` with a custom `MarshalAs` behaviour but it could also be defined as `[IntPtr]` with manual marshaling done inside the delegate. Finally the function returns/outputs the return type which PowerShell will handle automatically for you. It is also possible to predefine the delegate in the function by using a scriptblock without any code after the param block: $crypt = New-CtypesLib Crypt32.dll $crypt.Returns([bool]).SetLastError().CertEnumSystemStoreLocation = [Ordered]@{ Flags = [int] Arg = [IntPtr] Callback = { [OutputType([bool])] param ( [System.Runtime.InteropServices.MarshalAs([System.Runtime.InteropServices.UnmanagedType]::LPWStr)] [string]$StoreLocation, [int]$Flags, [IntPtr]$Reserved, [IntPtr]$Arg ) # Any code here will be ignored, this is only to dynamically create the delegate. } } $res = $crypt.CertEnumSystemStoreLocation(0, $null, { Write-Host $args[0] $true }) if (-not $res) { throw [System.ComponentModel.Win32Exception]$crypt.LastError } The scriptblock is run in the current thread and has access to the same scope as the caller. This means it can access the same variables as the outside scope instead of trying to marshal a managed object through an `[IntPtr]` like the first example. Note: ScriptBlock delegates are only supported in functions that will call the delegate in the same thread. Delegates which are run asynchronously will most likely crash the current process. VIEWING EXISTING DEFINITIONS Once a method has been pre-defined or when it is first called, the method is added as a `CodeMethod` member of the underlying library object. This ensures that the method does not need to be generated every time it is called and it provides an easy way to inspect the signature that was generated. As this is just any member on the Extended Type System (ETS) of the object, it can be retrieved using `Get-Member` or just by accessing the method like a property. For example $lib = New-CtypesLib Kernel32.dll $lib.Returns([IntPtr]).SetLastError().CreateFileW = [Ordered]@{ lpFileName = $lib.MarshalAs([string], "LPWStr") dwDesiredAccess = [int] dwShareMode = [System.IO.FileShare] lpSecurityAttributes = [IntPtr] dwCreationDisposition = [System.IO.FileMode] dwFlagsAndAttributes = [int] hTemplateFile = [IntPtr] } $lib.CreateFileW # CodeReference : IntPtr CreateFileW(System.Management.Automation.PSObject, System.String, Int32, System.IO.FileShare, IntPtr, System.IO.FileMode, Int32, IntPtr) # MemberType : CodeMethod # OverloadDefinitions : {static System.IntPtr CreateFileW(psobject lib, string lpFileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, System.IntPtr lpSecurityAttributes, # System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, System.IntPtr hTemplateFile)} # TypeNameOfValue : System.Management.Automation.PSCodeMethod # Value : static System.IntPtr CreateFileW(psobject lib, string lpFileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, System.IntPtr lpSecurityAttributes, # System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, System.IntPtr hTemplateFile) # Name : CreateFileW # IsInstance : True $lib | Get-Member -Name CreateFileW # TypeName: Ctypes.Library # Name MemberType Definition # ---- ---------- ---------- # CreateFileW CodeMethod static System.IntPtr CreateFileW(psobject lib, string lpFileName, int dwDesiredAccess, ... As each method is defined on the library object itself, they will not appear on the output of `New-CtypesLib` even if the same dll/library was specified. Note: The code is shown as static with the first argument being a `PSObject`. This is just a byproduct of the `PSCodeMethod` object, it is not static and the first argument can be ignored. REDEFINING THE SIGNATURE It is currently not possible to redefine the signature on an existing method. To use a new signature, either create a new Ctypes library object with `New-CtypesLib` and define the signature there or use the `EntryPoint()` method to define a new method for the same PInvoke function. For example: $lib.Returns([IntPtr]).SetLastError().CreateFileW = [Ordered]@{ lpFileName = $lib.MarshalAs([string], "LPWStr") dwDesiredAccess = [int] dwShareMode = [System.IO.FileShare] lpSecurityAttributes = [IntPtr] dwCreationDisposition = [System.IO.FileMode] dwFlagsAndAttributes = [int] hTemplateFile = [IntPtr] } $lib.SetLastError().CharSet('Unicode').EntryPoint('CreateFileW').CreateFileWithSA = [Ordered]@{ lpFileName = $lib.MarshalAs([string], "LPWStr") dwDesiredAccess = [int] dwShareMode = [System.IO.FileShare] lpSecurityAttributes = [SECURITY_ATTRIBUTES] dwCreationDisposition = [System.IO.FileMode] dwFlagsAndAttributes = [int] hTemplateFile = [IntPtr] } # Calls the IntPtr overload $lib.CreateFileW(...) # Calls the SECURITY_ATTRIBUTES overload $lib.CreateFileWithSA(...) This creates to methods that ultimately point to `CreateFileW`; one called `CreateFileW` and the other `CreateFileWithSA`. It is possible to now call both methods using the overloads they specified. There can be multiple Ctypes library objects for the same DLL without them interacting with each other. |