プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

.NET 8からはLibraryImportAttributeで使うSafeHandle型のコンストラクタをpublicにしよう

2023/12/02(土)追記: Breaking change: SafeHandle types must have public constructor - .NET | Microsoft Learnで本破壊的変更が明記されていました!

本記事のタイトルをより厳密に記述すると、「LibraryImportAttributeを使ったメソッドの戻り値やout引数、ref引数に自作のSafeHandle派生型を使っている場合は、.NET 8からはその型の引数なしコンストラクタのアクセス修飾子をpublicにする必要がある」です。以降、実験に使用したコードや、ドキュメントを調べた結果を記述します。

まとめ

  • LibraryImportAttributeで使用する自作SafeHandle型の引数なしコンストラクタは、.NET 7ではinternal等で問題ありませんが、.NET 8ではpublic必須な場合があります。
  • LibraryImportAttribute互換性のドキュメントを見るに、.NET 8での変更は意図的であるようです。 → Breaking change: SafeHandle types must have public constructor - .NET | Microsoft Learn記事が存在するため、確実に意図的な変更です。
  • .NET 8で引数なしコンストラクタをpublicにしていなかった場合に出るSYSLIB1051エラーが、メッセージからは全く原因が分からないのが辛いです……。

使用バージョン

.NETバージョンはSystem.Runtime.InteropServices.RuntimeInformation.FrameworkDescriptionで確認しました。

  • Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.8.0
  • .NET 7.0.14
  • .NET 8.0.0

動作検証用コード

自作SafeHandle型として、SafeThreadHandle型を定義します。その型の引数なしコンストラクタのアクセス修飾子がpublicかどうか、というのが本記事の主題です。型定義は後述します。なお、その型にした理由は、Microsoft.Win32.SafeHandles名前空間にSafeProcessHandle型はありますが、SafeThreadHandleは無いためです。

次に、LibraryImportAttributeを使用してSource Generatorを使用する、P/Invoke用のNativeMethods型を定義します:

using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

internal static partial class NativeMethods
{
    // 自作SafeHandle型を戻り値に使う例
    [LibraryImport("kernel32.dll", SetLastError = true)]
    internal static partial SafeThreadHandle OpenThread(
        uint dwDesiredAccess,
        [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
        uint dwThreadId);

    // 自作SafeHandle型をout引数に使う例
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool DuplicateHandle(
        SafeProcessHandle hSourceProcessHandle,
        SafeThreadHandle hSourceHandle,
        SafeProcessHandle hTargetProcessHandle,
        out SafeThreadHandle lpTargetHandle,
        uint dwDesiredAccess,
        [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
        uint dwOptions);

    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool CloseHandle(IntPtr hObject);
}

NativeMethods型の定義は、以降すべてで共通です。

.NET 7では、LibraryImportAttributeで使用する自作SafeHandle型の引数なしコンストラクタのアクセス修飾子がinternalでも問題なかった

本節では、<プロジェクト名>.csprojに以下の内容を使用します:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

自作SafeHandle型として、以下のコードを使用します:

using System;
using System.Runtime.InteropServices;

internal sealed class SafeThreadHandle : SafeHandle
{
    internal SafeThreadHandle() : base(IntPtr.Zero, true) { }
    public override bool IsInvalid => this.handle == IntPtr.Zero;
    protected override bool ReleaseHandle() => NativeMethods.CloseHandle(this.handle);
}

引数なしコンストラクタのアクセス修飾子がinternalであることに注目してください。その場合でも、.NET 7ではLibraryImportAttributeによるソース生成に成功します。生成結果です:

// <auto-generated/>
internal static unsafe partial class NativeMethods
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.9.1910")]
    [System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    internal static partial global::SafeThreadHandle OpenThread(uint dwDesiredAccess, bool bInheritHandle, uint dwThreadId)
    {
        int __lastError;
        bool __invokeSucceeded = default;
        int __bInheritHandle_native = default;
        global::SafeThreadHandle __retVal;
        System.IntPtr __retVal_native = default;
        // Setup - Perform required setup.
        __retVal = new global::SafeThreadHandle();
        try
        {
            // Marshal - Convert managed data to native data.
            __bInheritHandle_native = (int)(bInheritHandle ? 1 : 0);
            {
                System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
                __retVal_native = __PInvoke(dwDesiredAccess, __bInheritHandle_native, dwThreadId);
                __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();
            }

            __invokeSucceeded = true;
        }
        finally
        {
            if (__invokeSucceeded)
            {
                // GuaranteedUnmarshal - Convert native data to managed data even in the case of an exception during the non-cleanup phases.
                System.Runtime.InteropServices.Marshal.InitHandle(__retVal, __retVal_native);
            }
        }

        System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "OpenThread", ExactSpelling = true)]
        static extern unsafe System.IntPtr __PInvoke(uint dwDesiredAccess, int bInheritHandle, uint dwThreadId);
    }
}
internal static unsafe partial class NativeMethods
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.9.1910")]
    [System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    internal static partial bool DuplicateHandle(global::Microsoft.Win32.SafeHandles.SafeProcessHandle hSourceProcessHandle, global::SafeThreadHandle hSourceHandle, global::Microsoft.Win32.SafeHandles.SafeProcessHandle hTargetProcessHandle, out global::SafeThreadHandle lpTargetHandle, uint dwDesiredAccess, bool bInheritHandle, uint dwOptions)
    {
        int __lastError;
        bool __invokeSucceeded = default;
        System.Runtime.CompilerServices.Unsafe.SkipInit(out lpTargetHandle);
        System.IntPtr __hSourceProcessHandle_native = default;
        System.IntPtr __hSourceHandle_native = default;
        System.IntPtr __hTargetProcessHandle_native = default;
        System.IntPtr __lpTargetHandle_native = default;
        int __bInheritHandle_native = default;
        bool __retVal;
        int __retVal_native = default;
        // Setup - Perform required setup.
        bool hSourceProcessHandle__addRefd = false;
        bool hSourceHandle__addRefd = false;
        bool hTargetProcessHandle__addRefd = false;
        global::SafeThreadHandle lpTargetHandle__newHandle = new global::SafeThreadHandle();
        try
        {
            // Marshal - Convert managed data to native data.
            hSourceProcessHandle.DangerousAddRef(ref hSourceProcessHandle__addRefd);
            __hSourceProcessHandle_native = hSourceProcessHandle.DangerousGetHandle();
            hSourceHandle.DangerousAddRef(ref hSourceHandle__addRefd);
            __hSourceHandle_native = hSourceHandle.DangerousGetHandle();
            hTargetProcessHandle.DangerousAddRef(ref hTargetProcessHandle__addRefd);
            __hTargetProcessHandle_native = hTargetProcessHandle.DangerousGetHandle();
            __bInheritHandle_native = (int)(bInheritHandle ? 1 : 0);
            {
                System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
                __retVal_native = __PInvoke(__hSourceProcessHandle_native, __hSourceHandle_native, __hTargetProcessHandle_native, &__lpTargetHandle_native, dwDesiredAccess, __bInheritHandle_native, dwOptions);
                __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();
            }

            __invokeSucceeded = true;
            // Unmarshal - Convert native data to managed data.
            __retVal = __retVal_native != 0;
        }
        finally
        {
            if (__invokeSucceeded)
            {
                // GuaranteedUnmarshal - Convert native data to managed data even in the case of an exception during the non-cleanup phases.
                System.Runtime.InteropServices.Marshal.InitHandle(lpTargetHandle__newHandle, __lpTargetHandle_native);
                lpTargetHandle = lpTargetHandle__newHandle;
            }

            // Cleanup - Perform required cleanup.
            if (hSourceProcessHandle__addRefd)
                hSourceProcessHandle.DangerousRelease();
            if (hSourceHandle__addRefd)
                hSourceHandle.DangerousRelease();
            if (hTargetProcessHandle__addRefd)
                hTargetProcessHandle.DangerousRelease();
        }

        System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "DuplicateHandle", ExactSpelling = true)]
        static extern unsafe int __PInvoke(System.IntPtr hSourceProcessHandle, System.IntPtr hSourceHandle, System.IntPtr hTargetProcessHandle, System.IntPtr* lpTargetHandle, uint dwDesiredAccess, int bInheritHandle, uint dwOptions);
    }
}
// 今回の記事に無関係であるためCloseHandleは省略

自作SafeHandle型を戻り値とするOpenThreadメソッドの場合、以下のようにnew演算子でインスタンスを構築しています:

global::SafeThreadHandle __retVal;
// 略
__retVal = new global::SafeThreadHandle();

同様に、自作SafeHandle型をout引数にするDuplicateHandleメソッドの場合も、以下のようにnew演算子でインスタンスを構築しています:

global::SafeThreadHandle lpTargetHandle__newHandle = new global::SafeThreadHandle();

どちらの場合でも、自動生成されたソースはnew演算子で引数なしコンストラクタを呼び出しています。自動生成されたソースは同一アセンブリにコンパイルされることから、自作SafeHandle型の引数なしコンストラクタのアクセス修飾子がinternalである場合でも、問題なく呼び出せます。

.NET 8では、LibraryImportAttributeで使用する自作SafeHandle型の引数なしコンストラクタのアクセス修飾子がinternalだとSYSLIB1051エラーになる

本節では、<プロジェクト名>.csprojに以下の内容を使用します:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

自作SafeHandle型として、以下のコードを使用します:

using System;
using System.Runtime.InteropServices;

internal sealed class SafeThreadHandle : SafeHandle
{
    internal SafeThreadHandle() : base(IntPtr.Zero, true) { }
    public override bool IsInvalid => this.handle == IntPtr.Zero;
    protected override bool ReleaseHandle() => NativeMethods.CloseHandle(this.handle);
}

引き続き、引数なしコンストラクタのアクセス修飾子はinternalです。この場合、.NET 8では以下のエラーが発生します:

  • Error SYSLIB1051 The specified parameter needs to be marshalled from unmanaged to managed, but the marshaller type 'global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>' does not support it. The generated source will not handle marshalling of the return value of method 'OpenThread'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1051)
  • Error SYSLIB1051 The specified parameter needs to be marshalled from unmanaged to managed, but the marshaller type 'global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>' does not support it. The generated source will not handle marshalling of parameter 'lpTargetHandle'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1051)

エラーメッセージから、the marshaller type 'global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>' does not support itらしいこと、つまりSafeHandleMarshaller<global::SafeThreadHandle>型を使おうとしますが未サポートであるため失敗したことが分かります。しかし、引数なしコンストラクタのアクセス修飾子が問題であることは全くわかりません。辛いです。

.NET 8では、LibraryImportAttributeで使用する自作SafeHandle型の引数なしコンストラクタのアクセス修飾子はpublicにする必要がある

本節では、<プロジェクト名>.csprojに以下の内容を引き続き使用します:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

自作SafeHandle型として、以下のコードを使用します:

using System;
using System.Runtime.InteropServices;

internal sealed class SafeThreadHandle : SafeHandle
{
    public SafeThreadHandle() : base(IntPtr.Zero, true) { }
    public override bool IsInvalid => this.handle == IntPtr.Zero;
    protected override bool ReleaseHandle() => NativeMethods.CloseHandle(this.handle);
}

引数なしコンストラクタのアクセス修飾子をpublicへ変更しています。こうすると、.NET 8でもLibraryImportAttributeによるソース生成に成功します。生成結果です:

// <auto-generated/>
internal static unsafe partial class NativeMethods
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "8.0.9.3103")]
    [global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    internal static partial global::SafeThreadHandle OpenThread(uint dwDesiredAccess, bool bInheritHandle, uint dwThreadId)
    {
        int __lastError;
        bool __invokeSucceeded = default;
        int __bInheritHandle_native = default;
        global::SafeThreadHandle __retVal = default;
        nint __retVal_native = default;
        // Setup - Perform required setup.
        global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>.ManagedToUnmanagedOut __retVal_native__marshaller = new();
        try
        {
            // Marshal - Convert managed data to native data.
            __bInheritHandle_native = (int)(bInheritHandle ? 1 : 0);
            {
                global::System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
                __retVal_native = __PInvoke(dwDesiredAccess, __bInheritHandle_native, dwThreadId);
                __lastError = global::System.Runtime.InteropServices.Marshal.GetLastSystemError();
            }

            __invokeSucceeded = true;
            // UnmarshalCapture - Capture the native data into marshaller instances in case conversion to managed data throws an exception.
            __retVal_native__marshaller.FromUnmanaged(__retVal_native);
            // Unmarshal - Convert native data to managed data.
            __retVal = __retVal_native__marshaller.ToManaged();
        }
        finally
        {
            if (__invokeSucceeded)
            {
                // CleanupCalleeAllocated - Perform cleanup of callee allocated resources.
                __retVal_native__marshaller.Free();
            }
        }

        global::System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [global::System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "OpenThread", ExactSpelling = true)]
        static extern unsafe nint __PInvoke(uint __dwDesiredAccess_native, int __bInheritHandle_native, uint __dwThreadId_native);
    }
}
internal static unsafe partial class NativeMethods
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "8.0.9.3103")]
    [global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    internal static partial bool DuplicateHandle(global::Microsoft.Win32.SafeHandles.SafeProcessHandle hSourceProcessHandle, global::SafeThreadHandle hSourceHandle, global::Microsoft.Win32.SafeHandles.SafeProcessHandle hTargetProcessHandle, out global::SafeThreadHandle lpTargetHandle, uint dwDesiredAccess, bool bInheritHandle, uint dwOptions)
    {
        int __lastError;
        bool __invokeSucceeded = default;
        global::System.Runtime.CompilerServices.Unsafe.SkipInit(out lpTargetHandle);
        nint __hSourceProcessHandle_native = default;
        nint __hSourceHandle_native = default;
        nint __hTargetProcessHandle_native = default;
        nint __lpTargetHandle_native = default;
        int __bInheritHandle_native = default;
        bool __retVal = default;
        int __retVal_native = default;
        // Setup - Perform required setup.
        global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>.ManagedToUnmanagedOut __lpTargetHandle_native__marshaller = new();
        global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::Microsoft.Win32.SafeHandles.SafeProcessHandle>.ManagedToUnmanagedIn __hTargetProcessHandle_native__marshaller = new();
        global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>.ManagedToUnmanagedIn __hSourceHandle_native__marshaller = new();
        global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::Microsoft.Win32.SafeHandles.SafeProcessHandle>.ManagedToUnmanagedIn __hSourceProcessHandle_native__marshaller = new();
        try
        {
            // Marshal - Convert managed data to native data.
            __bInheritHandle_native = (int)(bInheritHandle ? 1 : 0);
            __hTargetProcessHandle_native__marshaller.FromManaged(hTargetProcessHandle);
            __hSourceHandle_native__marshaller.FromManaged(hSourceHandle);
            __hSourceProcessHandle_native__marshaller.FromManaged(hSourceProcessHandle);
            {
                // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned.
                __hTargetProcessHandle_native = __hTargetProcessHandle_native__marshaller.ToUnmanaged();
                __hSourceHandle_native = __hSourceHandle_native__marshaller.ToUnmanaged();
                __hSourceProcessHandle_native = __hSourceProcessHandle_native__marshaller.ToUnmanaged();
                global::System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
                __retVal_native = __PInvoke(__hSourceProcessHandle_native, __hSourceHandle_native, __hTargetProcessHandle_native, &__lpTargetHandle_native, dwDesiredAccess, __bInheritHandle_native, dwOptions);
                __lastError = global::System.Runtime.InteropServices.Marshal.GetLastSystemError();
            }

            __invokeSucceeded = true;
            // UnmarshalCapture - Capture the native data into marshaller instances in case conversion to managed data throws an exception.
            __lpTargetHandle_native__marshaller.FromUnmanaged(__lpTargetHandle_native);
            // Unmarshal - Convert native data to managed data.
            __retVal = __retVal_native != 0;
            lpTargetHandle = __lpTargetHandle_native__marshaller.ToManaged();
        }
        finally
        {
            if (__invokeSucceeded)
            {
                // CleanupCalleeAllocated - Perform cleanup of callee allocated resources.
                __lpTargetHandle_native__marshaller.Free();
            }

            // CleanupCallerAllocated - Perform cleanup of caller allocated resources.
            __hTargetProcessHandle_native__marshaller.Free();
            __hSourceHandle_native__marshaller.Free();
            __hSourceProcessHandle_native__marshaller.Free();
        }

        global::System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [global::System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "DuplicateHandle", ExactSpelling = true)]
        static extern unsafe int __PInvoke(nint __hSourceProcessHandle_native, nint __hSourceHandle_native, nint __hTargetProcessHandle_native, nint* __lpTargetHandle_native, uint __dwDesiredAccess_native, int __bInheritHandle_native, uint __dwOptions_native);
    }
}
// 今回の記事に無関係であるためCloseHandleは省略

自作SafeHandle型を戻り値とするOpenThreadメソッドの場合、以下のようにSystem.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T>.ManagedToUnmanagedOut型経由でインスタンスを構築しています:

global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>.ManagedToUnmanagedOut __retVal_native__marshaller = new();
// 略
__retVal_native__marshaller.FromUnmanaged(__retVal_native);
// Unmarshal - Convert native data to managed data.
__retVal = __retVal_native__marshaller.ToManaged();

同様に、自作SafeHandle型をout引数にするDuplicateHandleメソッドの場合も、以下のようにSystem.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T>.ManagedToUnmanagedOut型経由でインスタンスを構築しています:

global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<global::SafeThreadHandle>.ManagedToUnmanagedOut __lpTargetHandle_native__marshaller = new();
// 略
__lpTargetHandle_native__marshaller.FromUnmanaged(__lpTargetHandle_native);
// 略
lpTargetHandle = __lpTargetHandle_native__marshaller.ToManaged();

どちらの場合でも、自動生成されたソースはSystem.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T>.ManagedToUnmanagedOut Structを経由して、自作SafeHandle型インスタンスを構築しています。

本節で記述したように、メソッドの戻り値やout引数で自作SafeHandle型を使用する場合には、引数なしコンストラクタのアクセス修飾子はpublicである必要があります。一方でそれ以外の場合、つまり値渡し用途の引数で自作SafeHandle型を使用する場合は、引数なしコンストラクタを使わないためアクセス修飾子は自由であるはずです。

なぜ.NET 8では自作SafeHandle型の引数なしコンストラクタに要求されるアクセス修飾子が変化したのか

以降はドキュメント類を探し回った結果を記述します。

SafeHandleMarshaller<T>型とは

System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T> Classは.NET 8で追加された型です。しかしドキュメントを読んでも役立つ説明はほとんどありません。

SafeHandleMarshaller<T>型の追加を提案するissueを探すと[API Proposal]: Marshaller type to support SafeHandle in source-generated marshalling without hard-coding in the generator · Issue #74035にありました。提案issueによると、以下の理由で追加したいとのことです:

  • (当該issue作成時点で)SafeHandle型は、現在Source Generatorが特別にマーシャリングしている唯一の型です。
    • string型はかつてはSource Generatorが特別にマーシャリングしていましたが、.NET 7開発時点ですでに特別扱いでは無くなっています(筆者注: Utf16StringMarshallerなどを経由するようになった、という意味だと思います)。
  • Source Generatorが行っているSafeHandle型用の特別なマーシャリングは、他の組み込み型のマーシャリングと比較して、非常に複雑です。
  • 上記の理由のため、Source GeneratorがSafeHandle型をマーシャリングするためのコードを直接出力する代わりのための、マーシャリングを行う型を導入したいです。それにより、相互運用コードのサービス性(筆者注:よく分からず)が向上します。

なお提案Issueには、「ref引数とout引数のために(SafeHandleMarshaller<T>型の)型引数にnew()制約を追加することも考えましたが、in引数や値渡し引数を使用しているAPIに破壊的変更を加えることになるため、取り下げました」という旨のAlternative Designsセクションの記述があります。

ソースレベルでは、引数なしコンストラクタのアクセス修飾子がpublicでなくても、ソース生成そのものはさせる方針だったらしい痕跡がある

System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T>.ManagedToUnmanagedOutソースを探すと、以下の記述があります:

public ManagedToUnmanagedOut()
{
    _initialized = false;
    // SafeHandle out marshalling has always required parameterless constructors,
    // but it has never required them to be public.
    // We construct the handle now to ensure we don't cause an exception
    // before we are able to capture the unmanaged handle after the call.
    _newHandle = Activator.CreateInstance<T>()!;
}

コメントに、「SafeHandle型のout引数用マーシャリングでは、引数なしコンストラクタは要求してきた。しかしそれがpublicであることは要求してこなかった。そのためこの時点でインスタンス構築を試みて、成功するかを検証する」ということが書かれています。つまり、当該コードが記述された時点では、引数なしコンストラクタのアクセス修飾子にかかわらず、System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller<T>.ManagedToUnmanagedOutを経由してインスタンスを生成するコードを自動生成させる設計だったようです。

しかし後に設計が変わった模様

LibraryImportAttribute互換性のドキュメントを見ると、以下の記述があります:

Safe Handles
Due to trimming issues with NativeAOT's implementation of Activator.CreateInstance, we have decided to change our recommendation of providing a public parameterless constructor for ref, out, and return scenarios to a requirement. We already required a parameterless constructor of some visibility, so changing to a requirement matches our design principles of taking breaking changes to make interop more understandable and enforce more of our best practices instead of going out of our way to provide backward compatibility at increasing costs.

つまり、Activator.CreateInstanceではAOTコンパイル時に問題が起こったため、SafeHandle型をref引数やout引数、戻り値として使用する場合について、引数なしコンストラクタのアクセス修飾子をpublicにすることを、推奨要件から必須要件へ変更したとのことです。そのような理由で.NET 8リリースでは、引数なしコンストラクタのアクセス修飾子がpublicではない場合に、SYSLIB1051エラーが発生するようになったようです。

そもそもちゃんと、破壊的変更と明記されていました

2023/12/02(土)に、Breaking change: SafeHandle types must have public constructor - .NET | Microsoft Learn記事を見つけました。意図された破壊的変更です!

感想

私の手元のプロジェクトを.NET 7から.NET 8へ移行しようとしたときに、今回の問題へ遭遇しました。SYSLIB1051エラーが発生する理由が全く分からず、しばらく悩みました。SYSLIB1051エラーが発生しているメソッドの傾向から推測して、どうにか原因を突き止められました。大変でした。

本記事の内容にハマる人がどれほどいるかは分かりませんが、ハマった人の参考になれば幸いです。