構造体の「StructLayout」とは何をするものか?

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

COMについて勉強していたときに、

[StructLayout(LayoutKind.Explicit)]
public struct PROPVARIANT
{
    [FieldOffset(0)]
    public ushort vt;

    [FieldOffset(2)]
    public ushort wReserved1;

    [FieldOffset(4)]
    public ushort wReserved2;

    [FieldOffset(6)]
    public ushort wReserved3;

    [FieldOffset(8)]
    public IntPtr pwszVal;

    [FieldOffset(8)]
    public float fltVal;
}

みたいなコードが出てきた。

StructLayoutとは何か?調べたい。

前提

  • VisualStudio2022
  • 2023/10月時点で試したこと

しらべたこと

ufcppの複合型のレイアウトを見ながら勉強させていただいた。ありがとうございます。

StructLayoutはC#で使うものだが、一旦C++でstructを作ったときにどういうアラインメントになるかを見てから、イメージを膨らませてからC#の構造体を作って、StructLayoutを試そうと思う。

アラインメントとは?

下記参照... https://ufcpp.net/study/csharp/interop/memorylayout/

C++でアラインメントを試してみる

下記のようなコードを書いて、どういう配置になってるのか?試してみた。

#include <iostream>
#include <string>

struct MyStruct
{
    char A; // char=1バイト
    long B; // long=4バイト
    char C; // char=1バイト
} ;

MyStruct ms;

int main()
{
    auto pA = &ms.A;
    auto pB = &ms.B;
    auto pC = &ms.C;

    printf("Address A : %x \r\n", (int)pA);
    printf("Address B : %x \r\n", (int)pB);
    printf("Address C : %x \r\n", (int)pC);
    printf("\r\n");

    printf("Address B-A : %d \r\n", ((int)pB - (int)pA));
    printf("Address C-A : %d \r\n", ((int)pC - (int)pA));
    printf("sizeof(MyStruct) : %d \r\n", (int)sizeof(MyStruct));
}

結果は下記のようになった。

Address A : 7620e710
Address B : 7620e714
Address C : 7620e718

Address B-A : 4
Address C-A : 8
sizeof(MyStruct) : 12

次に、構造体を下記のように、2つ目のメンバのlongをlong longに変更すると、

struct MyStruct
{
    char A; // char=1バイト
    long long B; // long long =8バイト
    char C; // char=1バイト
} ;

下記のようになった。

Address A : 49a4e710
Address B : 49a4e718
Address C : 49a4e720

Address B-A : 8
Address C-A : 16
sizeof(MyStruct) : 24

上記2パターンを、x64、x86でビルドして実行してみたのだが、結果はどちらも同じ結果になった。(バイト数は変わらない)

下記のMSDocsによると、その構造体の中の一番大きなメンバに合わせて、アライメントが決まるらしい。

https://learn.microsoft.com/ja-jp/cpp/build/x64-software-conventions?view=msvc-170

image.png

なので、最大がlongのときは4で、最大がlong longのときは8であるっぽい。

C#でやってみる

書いたコード

ufcppの複合型のレイアウトを参考に、下記を書いてみた。

using System.Runtime.InteropServices;

namespace StructLayoutJikken
{
    [StructLayout(LayoutKind.Auto, Pack = 8)]
    struct Sample
    {
        public byte A;
        public long B;
        public byte C;
    }

    class Program
    {
        static unsafe void Main()
        {
            var a = default(Sample);
            var p = &a;
            var pa = &a.A;
            var pb = &a.B;
            var pc = &a.C;

            Console.WriteLine($"Address A = 0x{((int)p).ToString("x")}");
            Console.WriteLine($"Address pA = 0x{((int)pa).ToString("x")}");
            Console.WriteLine($"Address pB = 0x{((int)pb).ToString("x")}");
            Console.WriteLine($"Address pC = 0x{((int)pc).ToString("x")}");

            Console.WriteLine($@"サイズ: {sizeof(Sample)}
                A: {(long)pa - (long)p}
                B: {(long)pb - (long)p}
                C: {(long)pc - (long)p}
                ");
        }
    }
}

StructLayoutのパラメータには、下記のようなものがある。

LayoutKind

意味
Auto 自動。ええようにしてくれる。マネージドのみになるらしい。
Explicit Packで指定したアライメントにする。
各メンバーの位置を[FieldOffset]を書いて指定できる。
Sequential 定義した順番にメンバーを前から配置する。アライメントはPackで指定できる。
構造体の場合はこれが既定値。

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-7.0

Pack

規定値は0。(現在のプラットフォームの既定値を使う)

値は、0、1、2、4、8、16、32、64、128 でないといけない。

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-7.0

実験1

上のC#コードをそのまま動かすと、下記のようになった。

配置が自動調整されて、Bが先頭に来てる。またAとCが

Address A = 0x25b7e510
Address pA = 0x25b7e518
Address pB = 0x25b7e510
Address pC = 0x25b7e519
サイズ: 16
                A: 8
                B: 0
                C: 9

実験2

Packの値を消した。

[StructLayout(LayoutKind.Auto)]
struct Sample
{
    public byte A;
    public long B;
    public byte C;
}

結果、変わらず。

Address A = 0x2237eac0
Address pA = 0x2237eac8
Address pB = 0x2237eac0
Address pC = 0x2237eac9
サイズ: 16
                A: 8
                B: 0
                C: 9

実験3

Sequentialにした。

[StructLayout(LayoutKind.Sequential)]
struct Sample
{
    public byte A;
    public long B;
    public byte C;
}

A→B→Cの順番に並んだ。

Address A = 0xe0ffe758
Address pA = 0xe0ffe758
Address pB = 0xe0ffe760
Address pC = 0xe0ffe768
サイズ: 24
                A: 0
                B: 8
                C: 16

実験4

Explicitにして、FieldOffsetの値を0,16,32にした。

[StructLayout(LayoutKind.Explicit)]
struct Sample
{
    [FieldOffset(0)]
    public byte A;
    [FieldOffset(16)]
    public long B;
    [FieldOffset(32)]
    public byte C;
}

FieldOffsetで指定した位置に、各メンバーが並んだ。

Address A = 0x11b7e888
Address pA = 0x11b7e888
Address pB = 0x11b7e898
Address pC = 0x11b7e8a8
サイズ: 40
                A: 0
                B: 16
                C: 32

やってみた感想

C#のほうだと、Packの値を調節することで、構造体のメンバの位置を調節できた。

→COMを使うにあたり、PropVariantの共用体(union)をC#に直すときに、

  • LayoutKind.Explicitにして、
  • [FieldOffset(x)]の位置を重ねる

ということをするために使える。

逆に、C++で、Packに相当する設定が今回見つけられなかった。 C++でアラインメントを調製する方法はあるのかな?

参考

複合型のレイアウト

https://ufcpp.net/study/csharp/interop/memorylayout/

StructLayoutAttribute クラス

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.structlayoutattribute?view=net-7.0