1

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.

0

2 Answers 2

2

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

To serialize the struct to a byte array, use the same approach as if you were to marshal it for use with an unmanaged library, then write the resulting byte stream to disk instead:

using namespace System.Runtime.InteropServices

# Calculate in-memory size of struct = length of resulting byte stream
$streamLength = [Marshal]::SizeOf($config)
$structBytes  = [byte[]]::new($streamLength)
$structCopied = $false

# Allocate unmanaged memory, copy struct memory to it, then copy back to managed array
try {
    $memPtr = [IntPtr]::Zero
    $memPtr = [Marshal]::AllocHGlobal($streamLength)
    [Marshal]::StructureToPtr($config, $memPtr, $true)
    [Marshal]::Copy($memPtr, $structBytes, 0, $streamLength)
    $structCopied = $true
}
finally {
    if($memPtr -ne [IntPtr]::Zero){
        [Marshal]::FreeHGlobal($memPtr)
    }
}

# Write resulting byte stream to file
if($structCopied){
    try {
        $outFile = New-Item output.bin
        $outHandle = $outFile.OpenWrite()
        $outHandle.Write($structBytes, 0, $streamLength)
    }
    finally {
        if($outHandle -is [IDisposable]){
            $outHandle.Dispose()
        }
    }
}

This produces a byte stream of length 66, which fits what you'd expect given the struct layout:

  • 2 bytes for level
  • 64 (16 * 4) bytes for thresholds
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks. Do you also have a suggestion how to serialize the resulting object into a byte array / file?
@RichardW Depends on your requirements - what's the purpose of serializing to disk?
The generated blob goes eventually into a firmware implemented in C and is read by casting it to a type following the same layout as MyConfig above. For reasons I cannot simply generate code and compile the struct with the firmware and I can also not use a proper serialization format like flatbuffers. I am aware that this is risky and have prepared for that.
2

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. The problem has been reported in GitHub issue #18049

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.