Home > Enterprise >  Accessing fixed size array elements from C# in PowerShell
Accessing fixed size array elements from C# in PowerShell

Time:04-02

Looking for a way to access individual array elements (continuation of this question)

$CSharpCode = @"
using System;
namespace TestStructure
{
    public struct TestStructure
    {
       public byte Field1;
       public unsafe fixed byte Field2[4];
    }
}
"@

$cp = New-Object System.CodeDom.Compiler.CompilerParameters
$cp.CompilerOptions = '/unsafe'
Add-Type -TypeDefinition $CSharpCode -CompilerParameters $cp

function ConvertTo-Struct
{
  # Only accept struct types (sealed value types that are neither primitive nor enum)
  param(
    [Parameter(Mandatory = $true)]
    [ValidateScript({ $_.IsValueType -and $_.IsSealed -and -not($_.IsPrimitive -or $_.IsEnum) })]
    [Type]$TargetType,

    [Parameter(Mandatory = $true)]
    [byte[]]$BinaryData
  )

  # Start by calculating minimum size of the underlying memory allocation for the new struct
  $memSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type]$TargetType)

  # Make sure user actually passed enough data to initialize struct
  if($memSize -gt $BinaryData.Length){
    Write-Error "Not enough binary data to create an instance of [$($TargetType.FullName)]"
    return
  }

  # now we just need some unmanaged memory in which to create our struct instance
  $memPtr = [IntPtr]::Zero
  try {
    $memPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($memSize)

    # copy byte array to allocated unmanaged memory from previous step
    [System.Runtime.InteropServices.Marshal]::Copy($BinaryData, 0, $memPtr, $memSize)

    # then marshal the new memory region as a struct and return
    return [System.Runtime.InteropServices.Marshal]::PtrToStructure($memPtr, [type]$TargetType)
  }
  finally {
    # and finally remember to clean up the allocated memory
    if($memPtr -ne [IntPtr]::Zero){
      [System.Runtime.InteropServices.Marshal]::FreeHGlobal($memPtr)
    }
  }
}

$testStructure = ConvertTo-Struct -TargetType ([TestStructure.TestStructure]) -BinaryData (1..100 -as [byte[]])
$testStructure.Field1
$testStructure.Field2

this produces the following output:

1

FixedElementField
-----------------
                2
  • only the first array element of $Field2 is visible, can't access others using $testStructure.Field2[x]

Looking for a way to iterate over FixedBuffer of known type / size

$testStructure.Field2.GetType() says <Field2>e__FixedBuffer0 $testStructure.Field2.FixedElementField.GetType() is byte

  • can't see a way to access other elements of the array.

CodePudding user response:

PowerShell's type adapter doesn't really have anything in place to handle unsafe fixed byte[] fields, effectively raw pointers from the underlying type system's point of view.

The declared size of the underlying memory allocation is only stored in metadata, which you can locate as follows:

# Locate appropriate field metadata
$fieldInfo = $testStructure.GetType().GetField('Field2')

# Discover fixed buffer attribute
$fixedBufferAttribute = $fieldInfo.CustomAttributes.Where({$_.AttributeType -eq [System.Runtime.CompilerServices.FixedBufferAttribute]}) |Select -First 1

Now we can figure out the size and which array element type is expected:

if($fixedBufferAttribute)
{
    # Grab array element type   size from FixedBuffer attribute
    $elemType,$size = $fixedBufferAttribute.ConstructorArguments

    # Create array of appropriate size
    $values = $elemType.MakeArrayType()::new($size)

    # Copy values from fixed buffer pointer to managed array
    try {
        $fixedBufferHandle = [System.Runtime.InteropServices.GCHandle]::Alloc($TestStructure.Field2, 'Pinned')
        [System.Runtime.InteropServices.Marshal]::Copy($fixedBufferHandle.AddrOfPinnedObject(), $values, 0, $size)

        return $values
    }
    finally {
        $fixedBufferHandle.Free()
    }
}

You'll find $values now contains the expected byte values 2, 3, 4 and 5

  • Related