Search code examples
c#powershellpinvokepowershell-core

PowerShell: How to [ref] an element in array?


I'm working on coding a P/Invoke using PowerShell Core, but it fails.

using System;
using System.Runtime.InteropServices;

public class Bindings
{
    [DllImport("MyEncoder.dll")]
    public static extern bool EncodeStream(
        byte[] pbIn,
        int cbIn,
        ref byte pbOut,
        out int cbOut);
}

My C# code is as:

var pbOut = new byte[pbIn.Length];
int cbOut = 0;
Bindings.EncodeStream(pbIn, pbIn.Length, ref pbOut[0], out cbOut);

It works.

My PowerShell code is as:

$Bindings = Add-Type -TypeDefinition $cs_code -PassThru

[byte[]]$pbIn = [IO.File]::ReadAllBytes("src.txt")
$cbIn = $pbIn.Length

$pbOut = [byte[]]::new($cbIn)
$cbOut = [int]0

# PowerShell 7.3.9
# Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
$Bindings::EncodeStream($pbIn, $cbIn, [ref]$pbOut[0], [ref]$cbOut)

I tried debugging and found GCHandle.AddrOfPinnedObject returned different address for $pbOut $pbOut[0]. So I wonder if it's the point that $pbOut[0] created new value and PoewrShell referenced the temp value instead.

Any help and test is welcome!


Solution

  • PowerShell's [ref] type and C#'s ref keyword function very differently, and you fundamentally cannot obtain a reference to an individual array element in PowerShell.

    To work around this limitation, create a C# wrapper method around your P/Invoke method, which:

    • accepts a whole byte[] array from PowerShell and uses ref pbOut[0] internally in the P/Invoke call.
    • for easier use from PowerShell:
      • returns the cbOut value
      • throws an exception if an error occurs.

    Something along the following lines (untested):

    $Bindings = Add-Type -PassThru @'
    using System;
    using System.Runtime.InteropServices;
    
    public class Bindings
    {
        public static int EncodeStream(byte[] pbIn, byte[] pbOut)
        {
          int cbOut;
          if (!EncodeStream_Impl(pbIn, pbIn.Length, ref pbOut[0], out cbOut)) 
          {
            throw new Exception("Encoding failed.");
          }
          return cbOut;
        }
    
        [DllImport("MyEncoder.dll", EntryPoint="EncodeStream")]
        private static extern bool EncodeStream_Impl(
            byte[] pbIn,
            int cbIn,
            ref byte pbOut,
            out int cbOut);
    }
    '@
    
    [byte[]]$pbIn = [IO.File]::ReadAllBytes("src.txt")
    
    $pbOut = [byte[]]::new($pbIn.Length)
    
    $cbOut = $Bindings::EncodeStream($pbIn, $pbOut)