c# - 在结构中编码(marshal) IntPtr[] 会导致 midiStream 函数出现错误,但将数组展开到一堆字段是可行的

标签 c# windows pinvoke midi

我正在尝试使用 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 中用数组取消注释这两行,而是删除 Reserved0Reserved7 字段,它不会'不再工作了。发生的情况如下:

一切正常,直到并包括 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 无法正确编码,尝试这样做会破坏其他值。为了验证这一点,我创建了一个 Platform Invoke 测试项目:

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.

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

具有单独写入字段的版本可以工作,因为该结构是 blittable 的。因此 p/invoke 编码器通过固定与 native 结构二进制兼容的托管结构来优化调用。在您调用 midiOutUnprepareHeader 之前,GC 仍有机会重新定位该结构,但您似乎还没有被它捕获。如果您坚持使用 bittable 结构,您需要固定它直到调用 midiOutUnprepareHeader

因此,底线是您需要提供一个结构,该结构在您调用 midiOutUnprepareHeader 之前一直有效。个人建议使用Marshal.AllocHGlobalMarshal.StructureToPtr,然后Marshal.FreeHGlobal一次midiOutUnprepareHeader返回。并且显然将参数从 ref MidiHeader 切换为 IntPtr

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

关于c# - 在结构中编码(marshal) IntPtr[] 会导致 midiStream 函数出现错误,但将数组展开到一堆字段是可行的,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28925449/

相关文章:

c# - 从 .NET 2.0 向下迁移到 .NET 1.1 - DllImport 不再有效

java - 使用 Objective C 创建唯一用户 ID 的算法

c# - 使用 InstallShield LE 时,msi 包含一个程序集的过时版本,但包含其他程序集的当前版本

windows - 有什么方法可以在 Vista x64 上分配 4GB 以上的物理内存吗?

windows - 如何检查一个进程中可以使用的地址空间的最大内存量

C# IteropServices 与 C++ dll - 在 excel 中工作?

c# - 处理奇数场偏移

c# - 如何在 C# 中将 JSON 字符串解析为名称值对

c# - 如何通过输入和输出绑定(bind)连接 Azure Functions 2.0 CosmosDB?

python - Asyncio 错误,尝试对非套接字的对象进行操作