Home > Software engineering >  How to create a unmanaged struct in powershell containing plain arrays?
How to create a unmanaged struct in powershell containing plain arrays?

Time:09-07

I need to assemble a blob in a Powershell script that shows the following layout:

_Pragma("pack(1)")
struct MyConfig {
    uint16_t level;
    uint16_t thresholds[16];
    // ... the struct contains lots of POD members, but no pointers
}

struct MyConfig config = {
    .level = 1,
    .thresholds = {
        1, 2, 3, 4, ...
    }
};

The resulting config struct instance shall be dumped to a file.

I am able to solve the first part for integral types, but I am not able to access array members:

$typeCode = @'
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
[Serializable]
public struct MyConfig
{
    public Int16 level;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public Int32[] thresholds;
}
'@

Add-Type -TypeDefinition $typeCode;

$config = New-Object MyConfig;
$config.level = 1; # works
$config.thresholds[0] = 1; # Cannot index into a null array.

I have not looked at serializing the struct, yet.

CodePudding user response:

The [MarshalAs()] attribute you've decorated the thresholds member with is just metadata for marshalling data between unmanaged and managed memory - it doesn't actually do anything in the context of managed code.

To be able to assign to the array slots, you need to initialize thresholds with an actual array instance:

$config.thresholds = [int[]]::new(16)
$config.thresholds[0] = 1

You can also use PowerShell's cast-based object initializer syntax to initialize both members at once:

$config = [MyConfig]@{
    level = 1
    thresholds = [int[]]::new(16)
}
$config.thresholds[0] = 1

CodePudding user response:

To complement Mathias R. Jessen's helpful answer with background information and more convenient solutions:

C# structs (.NET value types) do not support members that are embedded arrays of fixed length:

It is only ever a reference (pointer) to an array (of unspecified size) that is stored in the struct itself, and that reference is null when an instance of the struct is created by default.

An alternative to allocating and assigning a fixed-size array to your .thresholds field after construction of an instance is to declare a public constructor that performs the array initialization.

In C# 10 / PowerShell (Core) 7.2. , you can do this as follows:

$typeCode = @'
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
[Serializable]
public struct MyConfig
{
    // Explicit default (parameterless) constructor that initializes the fields.
    // Note: ALL fields must be initialized.
    public MyConfig() { level = 0; thresholds = new Int32[16]; }
    public Int16 level;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public Int32[] thresholds;
}
'@

Add-Type -TypeDefinition $typeCode

# Construct an instance.
[MyConfig]::new()

The above prints the following to the display; the {0, 0, 0, 0…} part implies that the array was allocated:

{0, 0, 0, 0…}

In C# 9- / Windows PowerShell and earlier PowerShell (Core) versions), parameterless constructors aren't supported, so a workaround is needed:

Declare a constructor with a dummy parameter that has a default value:

$typeCode = @'
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
[Serializable]
public struct MyConfig
{
    // Note the `int unused = 0` dummy parameter.
    public MyConfig(int unused = 0) { level = 0; thresholds = new Int32[16]; }
    public Int16 level;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public Int32[] thresholds;
}
'@

Add-Type -TypeDefinition $typeCode

# Construct an instance.
# Do NOT use New-Object MyConfig (see below).
[MyConfig]::new()

Caveats:

  • The constructor with the optional parameter is only called if you use [MyConfig]::new() to construct an instance; New-Object MyConfig does not do that, even though you'd expect the two command forms to be equivalent.

  • In C# code, using default(MyConfig) does not call either constructor, because the purpose of default() is simply to zero out the structs members.

  • Related