在结构体中封送 IntPtr[] 会导致 midiStream 函数出错,但将数组展开到一堆字段是有效的



我正在尝试使用C#中的Windows多媒体MIDI函数。具体说来:

MMRESULT midiOutPrepareHeader(  HMIDIOUT hmo,  LPMIDIHDR lpMidiOutHdr,  UINT cbMidiOutHdr  );
MMRESULT midiOutUnprepareHeader(  HMIDIOUT hmo,  LPMIDIHDR lpMidiOutHdr,  UINT cbMidiOutHdr  );
MMRESULT midiStreamOut(  HMIDISTRM hMidiStream,  LPMIDIHDR lpMidiHdr,  UINT cbMidiHdr  );
MMRESULT midiStreamRestart(  HMIDISTRM hms  );
/* MIDI data block header */
typedef struct midihdr_tag {
    LPSTR       lpData;               /* pointer to locked data block */
    DWORD       dwBufferLength;       /* length of data in data block */
    DWORD       dwBytesRecorded;      /* used for input only */
    DWORD_PTR   dwUser;               /* for client's use */
    DWORD       dwFlags;              /* assorted flags (see defines) */
    struct midihdr_tag far *lpNext;   /* reserved for driver */
    DWORD_PTR   reserved;             /* reserved for driver */
#if (WINVER >= 0x0400)
    DWORD       dwOffset;             /* Callback offset into buffer */
    DWORD_PTR   dwReserved[8];        /* Reserved for MMSYSTEM */
#endif
} MIDIHDR, *PMIDIHDR, NEAR *NPMIDIHDR, FAR *LPMIDIHDR;

在 C 程序中,我可以通过执行以下操作成功使用这些函数:

HMIDISTRM hms;
midiStreamOpen(&hms, ...);
MIDIHDR hdr;
hdr.this = that; ...
midiStreamRestart(hms);
midiOutPrepareHeader(hms, &hdr, sizeof(MIDIHDR)); // sizeof(MIDIHDR) == 64
midiStreamOut(hms, &hdr, sizeof(MIDIHDR));
// wait for an event that is set from the midi callback when the playback has finished
WaitForSingleObject(...);
midiOutUnprepareHeader(hms, &hdr, sizeof(MIDIHDR));

上述调用序列有效且不会产生任何错误(为了可读性,省略了错误检查)。

为了在 C# 中使用它们,我创建了一些 P/Invoke 代码:

[DllImport("winmm.dll")]
public static extern int midiOutPrepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiOutUnprepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiStreamOut(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiStreamRestart(IntPtr handle);
[StructLayout(LayoutKind.Sequential)]
public struct MidiHeader
{
    public IntPtr Data;
    public uint BufferLength;
    public uint BytesRecorded;
    public IntPtr UserData;
    public uint Flags;
    public IntPtr Next;
    public IntPtr Reserved;
    public uint Offset;
    //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    //public IntPtr[] Reserved2;
    public IntPtr Reserved0;
    public IntPtr Reserved1;
    public IntPtr Reserved2;
    public IntPtr Reserved3;
    public IntPtr Reserved4;
    public IntPtr Reserved5;
    public IntPtr Reserved6;
    public IntPtr Reserved7;
}

调用顺序与 C 中的相同:

var hdr = new MidiHeader();
hdr.this = that;
midiStreamRestart(handle);
midiOutPrepareHeader(handle, ref header, headerSize); // headerSize == 64
midiStreamOut(handle, ref header, headerSize);
mre.WaitOne(); // wait until the midi playback has finished.
midiOutUnprepareHeader(handle, ref header, headerSize);

MIDI 输出工作,代码不产生错误(错误检查再次省略)。

一旦我取消注释MidiHeader数组中的两行,而是删除Reserved7字段的Reserved0,它就不再起作用了。发生的情况如下:

一切都很正常,直到并包括midiStreamOut.我可以听到 midi 输出。播放长度正确。但是,播放结束时永远不会调用事件回调。

此时,MidiHeader.Flags的值为 0xe ,表示流仍在播放(即使回调已收到播放已完成消息的通知)。MidiHeader.Flags的值应为9,表示流已完成播放。

midiOutUnprepareHeader的调用失败,错误代码0x41("无法在媒体数据仍在播放时执行此操作。 重置设备,或等待数据播放完毕。请注意,重置设备(如错误消息中所示)实际上并不能解决问题(等待或多次尝试也不能)。

另一个正常工作的变体是,如果我在 C# 声明的签名中使用 IntPtr 而不是ref MidiHeader,然后手动分配非托管内存,将我的 MidiHeader 结构复制到该内存上,然后使用分配的内存调用函数。

此外,我尝试将传递给headerSize参数的大小减小到 32。由于这些字段是保留的(事实上,在以前版本的Windows API中不存在),因此它们似乎没有特别的用途。但是,这并不能解决问题,即使 Windows 甚至不应该知道数组的存在,因此它不应该执行任何操作。再次完全注释掉数组可以解决问题(即,数组和 8 个Reserved*字段都被注释掉,headerSize为 32)。

这向我暗示IntPtr[] Reserved2无法正确封送,并且试图这样做会破坏其他值。为了验证这一点,我创建了一个平台调用测试项目:

WIN32PROJECT1_API void __stdcall test_function(struct test_struct_t *s)
{
    printf("%u %u %u %u %u %u %u %un", s->test0, s->test1, s->test2, s->test3, s->test4, s->test5, s->test6, s->test7);
    for (int i = 0; i < sizeof(s->pointer_array) / sizeof(s->pointer_array[0]); ++i)
    {
        printf("%u ", ((uint32_t)s->pointer_array[i]) >> 16);
    }
    printf("n");
}
typedef int32_t *test_ptr;
struct test_struct_t
{
    test_ptr test0;
    uint32_t test1;
    uint32_t test2;
    test_ptr test3;
    uint32_t test4;
    test_ptr test5;
    uint32_t test6;
    uint32_t test7;
    test_ptr pointer_array[8];
};

从 C# 调用:

[StructLayout(LayoutKind.Sequential)]
struct TestStruct
{
    public IntPtr test0;
    public uint test1;
    public uint test2;
    public IntPtr test3;
    public uint test4;
    public IntPtr test5;
    public uint test6;
    public uint test7;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public IntPtr[] pointer_array;
}
[DllImport("Win32Project1.dll")]
static extern void test_function(ref TestStruct s);
static void Main(string[] args)
{
    TestStruct s = new TestStruct();
    s.test0 = IntPtr.Zero;
    s.test1 = 1;
    s.test2 = 2;
    s.test3 = IntPtr.Add(IntPtr.Zero, 3);
    s.test4 = 4;
    s.test5 = IntPtr.Add(IntPtr.Zero, 5);
    s.test6 = 6;
    s.test7 = 7;
    s.pointer_array = new IntPtr[8];
    for (int i = 0; i < s.pointer_array.Length; ++i)
    {
        s.pointer_array[i] = IntPtr.Add(IntPtr.Zero, i << 16);
    }
    test_function(ref s);
    Console.ReadLine();
}

并且输出符合预期,因此IntPtr[] pointer_array的封送处理在此程序中有效。

我知道不使用SafeHandle是次优的,但是,当使用它时,MIDI 函数在使用数组时的行为更加奇怪,所以我选择一次解决一个问题。

为什么使用IntPtr[] Reserved2会导致错误?


下面是一些生成完整示例的代码:

C 代码

/*
* example9.c
*
*  Created on: Dec 21, 2011
*      Author: David J. Rager
*       Email: djrager@fourthwoods.com
*
* This code is hereby released into the public domain per the Creative Commons
* Public Domain dedication.
*
* http://http://creativecommons.org/publicdomain/zero/1.0/
*/
#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>
HANDLE event;
static void CALLBACK example9_callback(HMIDIOUT out, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
    switch (msg)
    {
    case MOM_DONE:
        SetEvent(event);
        break;
    case MOM_POSITIONCB:
    case MOM_OPEN:
    case MOM_CLOSE:
        break;
    }
}
int main()
{
    unsigned int streambufsize = 24;
    char* streambuf = NULL;
    HMIDISTRM out;
    MIDIPROPTIMEDIV prop;
    MIDIHDR mhdr;
    unsigned int device = 0;
    streambuf = (char*)malloc(streambufsize);
    if (streambuf == NULL)
        goto error2;
    memset(streambuf, 0, streambufsize);
    if ((event = CreateEvent(0, FALSE, FALSE, 0)) == NULL)
        goto error3;
    memset(&mhdr, 0, sizeof(mhdr));
    mhdr.lpData = streambuf;
    mhdr.dwBufferLength = mhdr.dwBytesRecorded = streambufsize;
    mhdr.dwFlags = 0;
    // flags and event code
    mhdr.lpData[8] = (char)0x90;
    mhdr.lpData[9] = 63;
    mhdr.lpData[10] = 0x55;
    mhdr.lpData[11] = 0;
    // next event
    mhdr.lpData[12] = 96; // delta time?
    mhdr.lpData[20] = (char)0x80;
    mhdr.lpData[21] = 63;
    mhdr.lpData[22] = 0x55;
    mhdr.lpData[23] = 0;

    if (midiStreamOpen(&out, &device, 1, (DWORD)example9_callback, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
        goto error4;
    //printf("sizeof midiheader = %dn", sizeof(MIDIHDR));
    if (midiOutPrepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
        goto error5;
    if (midiStreamRestart(out) != MMSYSERR_NOERROR)
        goto error6;
    if (midiStreamOut(out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
        goto error7;
    WaitForSingleObject(event, INFINITE);
error7:
    //midiOutReset((HMIDIOUT)out);
error6:
    MMRESULT blah = midiOutUnprepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR));
    printf("stuff: %dn", blah);
error5:
    midiStreamClose(out);
error4:
    CloseHandle(event);
error3:
    free(streambuf);
error2:
    //free(tracks);
error1:
    //free(hdr);
    return(0);
}

C# 代码

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace MidiOutTest
{
    class Program
    {
        [DllImport("winmm.dll")]
        public static extern int midiStreamOpen(out IntPtr handle, ref uint deviceId, uint cMidi, MidiCallback callback, IntPtr userData, uint flags);
        [DllImport("winmm.dll")]
        public static extern int midiStreamOut(IntPtr handle, ref MidiHeader header, uint headerSize);
        [DllImport("winmm.dll")]
        public static extern int midiStreamRestart(IntPtr handle);
        [DllImport("winmm.dll")]
        public static extern int midiOutPrepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
        [DllImport("winmm.dll")]
        public static extern int midiOutUnprepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
        [DllImport("winmm.dll", CharSet = CharSet.Unicode)]
        public static extern int midiOutGetErrorText(int mmsyserr, StringBuilder errMsg, int capacity);
        [DllImport("winmm.dll")]
        public static extern int midiStreamClose(IntPtr handle);
        public delegate void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2);
        private static readonly ManualResetEvent mre = new ManualResetEvent(false);
        private static void TestMidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
        {
            Debug.WriteLine(msg.ToString());
            if (msg == MOM_DONE)
            {
                Debug.WriteLine("MOM_DONE");
                mre.Set();
            }
        }
        public const uint MOM_DONE = 0x3C9;
        public const int MMSYSERR_NOERROR = 0;
        public const int MAXERRORLENGTH = 256;
        public const uint CALLBACK_FUNCTION = 0x30000;
        public const uint MidiHeaderSize = 64;
        public static void CheckMidiOutMmsyserr(int mmsyserr)
        {
            if (mmsyserr != MMSYSERR_NOERROR)
            {
                var sb = new StringBuilder(MAXERRORLENGTH);
                var errorResult = midiOutGetErrorText(mmsyserr, sb, sb.Capacity);
                if (errorResult != MMSYSERR_NOERROR)
                {
                    throw new /*Midi*/Exception("An error occurred and there was another error while attempting to retrieve the error message."/*, mmsyserr*/);
                }
                throw new /*Midi*/Exception(sb.ToString()/*, mmsyserr*/);
            }
        }
        static void Main(string[] args)
        {
            IntPtr handle;
            uint deviceId = 0;
            CheckMidiOutMmsyserr(midiStreamOpen(out handle, ref deviceId, 1, TestMidiCallback, IntPtr.Zero, CALLBACK_FUNCTION));
            try
            {
                var bytes = new byte[24];
                IntPtr buffer = Marshal.AllocHGlobal(bytes.Length);
                try
                {
                    MidiHeader header = new MidiHeader();
                    header.Data = buffer;
                    header.BufferLength = 24;
                    header.BytesRecorded = 24;
                    header.UserData = IntPtr.Zero;
                    header.Flags = 0;
                    header.Next = IntPtr.Zero;
                    header.Reserved = IntPtr.Zero;
                    header.Offset = 0;
#warning uncomment if using array
                    //header.Reserved2 = new IntPtr[8];
                    // flags and event code
                    bytes[8] = 0x90;
                    bytes[9] = 63;
                    bytes[10] = 0x55;
                    bytes[11] = 0;
                    // next event
                    bytes[12] = 96;
                    bytes[20] = 0x80;
                    bytes[21] = 63;
                    bytes[22] = 0x55;
                    bytes[23] = 0;
                    Marshal.Copy(bytes, 0, buffer, bytes.Length);
                    CheckMidiOutMmsyserr(midiStreamRestart(handle));
                    CheckMidiOutMmsyserr(midiOutPrepareHeader(handle, ref header, MidiHeaderSize));
                    CheckMidiOutMmsyserr(midiStreamOut(handle, ref header, MidiHeaderSize));
                    mre.WaitOne();
                    CheckMidiOutMmsyserr(midiOutUnprepareHeader(handle, ref header, MidiHeaderSize));
                }
                finally
                {
                    Marshal.FreeHGlobal(buffer);
                }
            }
            finally
            {
                midiStreamClose(handle);
            }
        }
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct MidiHeader
    {
        public IntPtr Data;
        public uint BufferLength;
        public uint BytesRecorded;
        public IntPtr UserData;
        public uint Flags;
        public IntPtr Next;
        public IntPtr Reserved;
        public uint Offset;
#if false
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public IntPtr[] Reserved2;
#else
        public IntPtr Reserved0;
        public IntPtr Reserved1;
        public IntPtr Reserved2;
        public IntPtr Reserved3;
        public IntPtr Reserved4;
        public IntPtr Reserved5;
        public IntPtr Reserved6;
        public IntPtr Reserved7;
#endif
    }
}

来自midiOutPrepareHeader的文档:

在将 MIDI 数据块传递给设备驱动程序之前,必须通过将缓冲区传递给 midiOutPrepareHeader 函数来准备缓冲区。准备好标头后,不要修改缓冲区。驱动程序使用缓冲区完成后,调用 midiOutUnprepareHeader 函数。

你没有遵守这一点。编组器创建结构的临时本机版本,该版本在调用midiOutPrepareHeader期间存在。一旦midiOutPrepareHeader返回,临时本机结构就会被销毁。但是 MIDI 代码仍然有对它的引用。这是关键点,MIDI 代码包含对结构的引用,并且需要能够访问它。

具有单独写入字段的版本有效,因为该结构是可 blitit的。因此,p/invoke 封送器通过固定与本机结构二进制兼容的托管结构来优化调用。在您调用 GC 之前,GC 仍然有机会重新定位结构midiOutUnprepareHeader但似乎您还没有被抓住。如果你坚持使用位表结构,你需要固定它,直到你调用midiOutUnprepareHeader

因此,底线是您需要提供一个结构,该结构一直存在到您调用midiOutUnprepareHeader。就个人而言,我建议您使用Marshal.AllocHGlobalMarshal.StructureToPtr,然后在midiOutUnprepareHeader返回后Marshal.FreeHGlobal。并且显然将参数从ref MidiHeader切换到IntPtr.

我认为我不需要向您展示任何代码,因为从您的问题中可以清楚地看出您知道如何做这些事情。事实上,我提出的解决方案是你已经尝试和观察工作的解决方案。但现在你知道为什么了!

最新更新