使用.NET 7 LibraryImport封送函数指针

kcrjzv8t  于 2023-02-06  发布在  .NET
关注(0)|答案(1)|浏览(619)

我正尝试使用新的LibraryImport属性(与旧的DllImport相反)实现一些P/Invoke代码。具体来说,我正尝试封送WNDCLASSEXW结构体以便在RegisterClassEx中使用。
下面是我的WNDCLASSEXW托管实现的简化、缩短版本:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WindowClass
{
    private uint StructSize;
    public WindowClassStyle Style;
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public Win32API.WindowProcedure? WindowProcedure;
    private int ClassAdditionalBytes;
    private int WindowAdditionalBytes;
    public IntPtr Instance;
    public IntPtr Icon;
    public IntPtr Cursor;
    public IntPtr BackgroundBrush;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string? ClassMenuResourceName;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string? ClassName;
    public IntPtr SmallIcon;
}

我对Win32API.WindowProcedure的定义是:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate nint WindowProcedure(IntPtr windowHandle, MessageID messageID, nuint wParam, nint lParam);

最后是我对RegisterClassEx的定义:

[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
public static partial ushort RegisterClassEx(in WindowClass classDefinition);

但是,这会导致错误:
错误SYSLIB 1051:源生成的P/Invoke不支持类型“xxx.WindowClass”。生成的源将不处理参数“classDefinition”的封送处理。
因此,我认为我需要为WindowClass结构定制编组。
然而,由于这个系统相对较新,我很难找到正确和最佳的指导,以前,DllImport会神奇地编组大多数类型,几乎没有指导,但LibraryImport似乎需要更多的信息,而且有点严格。
我可以通过将类型更改为IntPtr并要求在程序中的其他地方将delegate转换为IntPtr来绕过这个问题,但我更愿意尽可能靠近托管/非托管边界进行转换,并保持结构体和公开的本机函数可用于更具描述性的类型。
我在搜索时找到的一些资源:

我更喜欢使用readonly struct,并将所有成员转换为{ get; init; }属性而不是字段,因为语义更好。但是我注意到MarshalAs属性不能应用于属性。是否有一个好方法既可以使用readonly struct属性,同时还提供必要的信息以确保所有内容都正确地编组入/编组出??特别是对于更复杂的类型,如string? <-> LPCWSTRdelegate? <-> void*和我可能遇到的其他此类类型。

附加问题:LibraryImport似乎不再强调指定正确调用约定的重要性。它不再像DllImport那样是主属性的一部分,而是使用一个辅助属性,如下所示:[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })],坦率地说看起来很糟糕。现在指定调用约定是必要的还是有益的?

b4lqfgs4

b4lqfgs41#

我能够让它与自定义封送处理一起工作。虽然Simon关于更改结构以包含本机类型的建议在一般情况下是有意义的,但在我的情况下却没有意义,因为这些类型将暴露给其他人使用。
对于更快、更频繁调用的方法,答案可能不同,但在这种情况下,注册一个类和创建一个窗口本质上是一个非常昂贵的操作,因此向/从不同的结构体复制数据的额外开销不值得关注。
封送拆收器是这样实现的:

[CustomMarshaller(typeof(WindowClass), MarshalMode.UnmanagedToManagedIn, typeof(WindowClassMarshaler))]
[CustomMarshaller(typeof(WindowClass), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
internal static unsafe class WindowClassMarshaler
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal unsafe struct WindowClassUnmanaged
    {
        public uint StructSize;
        public uint Style;
        public IntPtr WindowProcedure;
        public int ClassAdditionalBytes;
        public int WindowAdditionalBytes;
        public IntPtr Instance;
        public IntPtr Icon;
        public IntPtr Cursor;
        public IntPtr BackgroundBrush;
        public char* ClassMenuResourceName;
        public char* ClassName;
        public IntPtr SmallIcon;
    }

    internal static unsafe WindowClass ConvertToManaged(WindowClassUnmanaged unmanaged)
    {
        return new()
        {
            WindowProcedure = Marshal.GetDelegateForFunctionPointer<Win32API.WindowProcedure>(unmanaged.WindowProcedure),
            ClassMenuResourceName = MarshalHelpers.Win32WideCharArrToString(unmanaged.ClassMenuResourceName),
            ClassName = MarshalHelpers.Win32WideCharArrToString(unmanaged.ClassName),
            // (remainder omitted, just simple copies)
        };
    }

    internal unsafe ref struct ManagedToUnmanagedIn
    {
        public static int BufferSize => sizeof(WindowClassUnmanaged);

        private byte* UnmanagedBufferStruct;
        private char* UnmanagedStrResourceName, UnmanagedStrClassName;

        public void FromManaged(WindowClass managed, Span<byte> buffer)
        {
            IntPtr WindowProcedure = (managed.WindowProcedure == null) ? IntPtr.Zero : Marshal.GetFunctionPointerForDelegate(managed.WindowProcedure);
            this.UnmanagedStrResourceName = (managed.ClassMenuResourceName == null) ? null : (char*)Marshal.StringToHGlobalUni(managed.ClassMenuResourceName);
            this.UnmanagedStrClassName = (managed.ClassName == null) ? null : (char*)Marshal.StringToHGlobalUni(managed.ClassName);

            WindowClassUnmanaged Result = new()
            {
                WindowProcedure = WindowProcedure,
                ClassMenuResourceName = this.UnmanagedStrResourceName,
                ClassName = this.UnmanagedStrClassName,
                // (remainder omitted, just simple copies)
            };

            Span<byte> ResultByteView = MemoryMarshal.Cast<WindowClassUnmanaged, byte>(MemoryMarshal.CreateSpan(ref Result, 1));
            Debug.Assert(buffer.Length >= ResultByteView.Length, "Target buffer isn't large enough to hold the struct data.");
            ResultByteView.CopyTo(buffer);

            this.UnmanagedBufferStruct = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer));
        }

        public byte* ToUnmanaged() => this.UnmanagedBufferStruct;

        public void Free()
        {
            if (this.UnmanagedStrResourceName != null)
            {
                Marshal.FreeHGlobal((nint)this.UnmanagedStrResourceName);
                this.UnmanagedStrResourceName = null;
            }
            if (this.UnmanagedStrClassName != null)
            {
                Marshal.FreeHGlobal((nint)this.UnmanagedStrClassName);
                this.UnmanagedStrClassName = null;
            }
        }
    }
}

使用此辅助函数将Win32 LP(C)WSTR转换为常规.NET string

public static unsafe string? Win32WideCharArrToString(char* unmanagedArr)
{
    if (unmanagedArr == null) { return null; }
    int Length = 0;
    while (*(unmanagedArr + Length) != 0x0000) { Length++; }
    return Encoding.Unicode.GetString((byte*)unmanagedArr, Length * sizeof(char));
}

更好的WindowClass结构体与以前几乎相同,除了只读,并且所有元素都是{ get; init; }。成员上的MarshalAs属性不再需要,因为自定义封送处理处理所有事情。
最后,实际的extern函数现在看起来如下所示:

[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })]
public static partial ushort RegisterClassEx([MarshalUsing(typeof(WindowClassMarshaler))] WindowClass classDefinition);

注意这已经被修正了。以前我在参数上使用了in关键字,但是这会导致它传递一个指针指向结构数据的指针,这是一个额外的间接层,会导致代码失败。以上是正确工作的更新版本。
我已经在常规发布模式和AOT编译中测试并验证了这一点,这也是在本例中使用LibraryImport的原因。
但是,我的额外问题仍然存在,使用UnmanagedCallConv指定stdcall有什么好处吗?

相关问题