其他分享
首页 > 其他分享> > 封送结构中的IntPtr []导致midiStream函数错误,但将数组展开到一堆字段工作

封送结构中的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中的数组的两行注释,而是删除了Reserved0到Reserved7字段,它将不再起作用.发生了以下情况:

一切正常,直到包括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 %u\n", 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 = %d\n", 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: %d\n", 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的文档中:

Before you pass a MIDI data block to a device driver, you must prepare the buffer by passing it to the midiOutPrepareHeader function. After the header has been prepared, do not modify the buffer. After the driver is done using the buffer, call the midiOutUnprepareHeader function.

您不遵守这一点.编组器将为您的结构创建一个临时的本地版本,该版本在对midiOutPrepareHeader的调用期间一直存在.一旦midiOutPrepareHeader返回,则临时本机结构将被销毁.但是MIDI代码仍然有对其的引用.这就是关键点,MIDI代码拥有对您的结构的引用,需要能够对其进行访问.

带有单独编写字段的版本可以使用,因为该结构是可blittable的.因此,p / invoke marshaller通过固定与本机结构二进制兼容的托管结构来优化调用.在您调用midiOutUnprepareHeader之前,GC仍有重定位结构的机会,但是似乎您还没有被它所吸引.如果您坚持使用位表结构,则需要将其固定,直到您调用midiOutUnprepareHeader.

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

我认为不需要向您显示任何代码,因为从您的问题中可以清楚地知道您知道如何执行此操作.实际上,我建议的解决方案是您已经尝试并观察过的解决方案.但是现在你知道为什么了!

标签:midi,windows,c,pinvoke
来源: https://codeday.me/bug/20191120/2045485.html