views:

358

answers:

3

I'm having trouble calling functions of a native library from within managed C# code. I am developing for the 3.5 compact framework (Windows Mobile 6.x) just in case this would make any difference.

I am working with the waveIn* functions from coredll.dll (these are in winmm.dll in regular Windows I believe). This is what I came up with:

// namespace winmm; class winmm
[StructLayout(LayoutKind.Sequential)]
public struct WAVEFORMAT
{
    public ushort wFormatTag;
    public ushort nChannels;
    public uint nSamplesPerSec;
    public uint nAvgBytesPerSec;
    public ushort nBlockAlign;
    public ushort wBitsPerSample;
    public ushort cbSize;
}
[StructLayout(LayoutKind.Sequential)]
public struct WAVEHDR
{
    public IntPtr lpData;
    public uint dwBufferLength;
    public uint dwBytesRecorded;
    public IntPtr dwUser;
    public uint dwFlags;
    public uint dwLoops;
    public IntPtr lpNext;
    public IntPtr reserved;
}

public delegate void AudioRecordingDelegate(IntPtr deviceHandle, uint message, IntPtr instance, ref WAVEHDR wavehdr, IntPtr reserved2);

[DllImport("coredll.dll")]
public static extern int waveInAddBuffer(IntPtr hWaveIn, ref WAVEHDR lpWaveHdr, uint cWaveHdrSize);
[DllImport("coredll.dll")]
public static extern int waveInPrepareHeader(IntPtr hWaveIn, ref WAVEHDR lpWaveHdr, uint Size);
[DllImport("coredll.dll")]
public static extern int waveInStart(IntPtr hWaveIn);

// some other class
private WinMM.WinMM.AudioRecordingDelegate waveIn;
private IntPtr handle;
private uint bufferLength;

private void setupBuffer()
{
    byte[] buffer = new byte[bufferLength];
    GCHandle bufferPin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    WinMM.WinMM.WAVEHDR hdr = new WinMM.WinMM.WAVEHDR();
    hdr.lpData = bufferPin.AddrOfPinnedObject();
    hdr.dwBufferLength = this.bufferLength;
    hdr.dwFlags = 0;

    int i = WinMM.WinMM.waveInPrepareHeader(this.handle, ref hdr, Convert.ToUInt32(Marshal.SizeOf(hdr)));
    if (i != WinMM.WinMM.MMSYSERR_NOERROR)
    {
        this.Text = "Error: waveInPrepare";
        return;
    }
    i = WinMM.WinMM.waveInAddBuffer(this.handle, ref hdr, Convert.ToUInt32(Marshal.SizeOf(hdr)));
    if (i != WinMM.WinMM.MMSYSERR_NOERROR)
    {
        this.Text = "Error: waveInAddrBuffer";
        return;
    }
}

private void setupWaveIn()
{
    WinMM.WinMM.WAVEFORMAT format = new WinMM.WinMM.WAVEFORMAT();
    format.wFormatTag = WinMM.WinMM.WAVE_FORMAT_PCM;
    format.nChannels = 1;
    format.nSamplesPerSec = 8000;
    format.wBitsPerSample = 8;
    format.nBlockAlign = Convert.ToUInt16(format.nChannels * format.wBitsPerSample);
    format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
    this.bufferLength = format.nAvgBytesPerSec;
    format.cbSize = 0;

    int i = WinMM.WinMM.waveInOpen(out this.handle, WinMM.WinMM.WAVE_MAPPER, ref format, Marshal.GetFunctionPointerForDelegate(waveIn), 0, WinMM.WinMM.CALLBACK_FUNCTION);
    if (i != WinMM.WinMM.MMSYSERR_NOERROR) 
    {
        this.Text = "Error: waveInOpen";
        return;
    }

    setupBuffer();

    WinMM.WinMM.waveInStart(this.handle);
}

I read alot about marshalling the last few days, nevertheless I do not get this code working. When my callback function is called (waveIn) when the buffer is full, the hdr structure passed back in wavehdr is obviously corrupted. Here is an examlpe of how the structure looks like at that point:

-      wavehdr {WinMM.WinMM.WAVEHDR}   WinMM.WinMM.WAVEHDR
         dwBufferLength 0x19904c00  uint
         dwBytesRecorded    0x0000fa00  uint
         dwFlags    0x00000003  uint
         dwLoops    0x1990f6a4  uint
+       dwUser  0x00000000  System.IntPtr
+       lpData  0x00000000  System.IntPtr
+       lpNext  0x00000000  System.IntPtr
+       reserved    0x7c07c9a0  System.IntPtr

This obiously is not what I expected to get passed. I am clearly concerned about the order of the fields in the view. I do not know if Visual Studio .NET cares about actual memory order when displaying the record in the "local"-view, but they are obviously not displayed in the order I speciefied in the struct.

Then theres no data pointer and the bufferLength field is far to high. Interestingly the bytesRecorded field is exactly 64000 - bufferLength and bytesRecorded I'd expect both to be 64000 though. I do not know what exactly is going wrong, maybe someone can help me out on this. I'm an absolute noob to managed code programming and marshalling so please don't be too harsh to me for all the stupid things I've propably done.

Oh here's the C code definition for WAVEHDR which I found here, I believe I might have done something wrong in the C# struct definition:

/* wave data block header */
typedef struct wavehdr_tag {
    LPSTR       lpData;                 /* pointer to locked data buffer */
    DWORD       dwBufferLength;         /* length of data buffer */
    DWORD       dwBytesRecorded;        /* used for input only */
    DWORD_PTR   dwUser;                 /* for client's use */
    DWORD       dwFlags;                /* assorted flags (see defines) */
    DWORD       dwLoops;                /* loop control counter */
    struct wavehdr_tag FAR *lpNext;     /* reserved for driver */
    DWORD_PTR   reserved;               /* reserved for driver */
} WAVEHDR, *PWAVEHDR, NEAR *NPWAVEHDR, FAR *LPWAVEHDR;

If you are used to work with all those low level tools like pointer-arithmetic, casts, etc starting writing managed code is a pain in the ass. It's like trying to learn how to swim with your hands tied on your back. Some things I tried (to no effect): .NET compact framework does not seem to support the Pack = 2^x directive in [StructLayout]. I tried [StructLayout(LayoutKind.Explicit)] and used 4 bytes and 8 bytes alignment. 4 bytes alignmentgave me the same result as the above code and 8 bytes alignment only made things worse - but that's what I expected. Interestingly if I move the code from setupBuffer into the setupWaveIn and do not declare the GCHandle in the context of the class but in a local context of setupWaveIn the struct returned by the callback function does not seem to be corrupted. I am not sure however why this is the case and how I can use this knowledge to fix my code. Forget that. I mixed up stuff with much older code I used.

I'd really appreciate any good links on marshalling, calling unmanaged code from C#, etc. Then I'd be very happy if someone could point out my mistakes. What am I doing wrong? Why do I not get what I'd expect.

+2  A: 

Your P/Invoke declarations are impeccable. However, there's something very odd about the WAVEHDR dump you posted. It is missing the lpData field. If you insert that, all the numbers line up properly (i.e. lpData = 0x19904c00, dwBufferLength = 0x0000fa00, etc).

Not sure how this came about, but it somehow is using the wrong WAVEHDR declaration. WinMM.WinMM ought to be a hint.

Hans Passant
It's not missing it, but it is at the end with the other IntPtr objects. I found that beeing very odd but am not sure if I can tell the memory position of a field by it's position in the local view of VS.net.
Daniel Baulig
Yes, very odd. Debugger bug? Do you actually get garbage when you read the dwBufferLength field?
Hans Passant
Yes it did. I changed some stuff and moved the byte[] buffer and the GCHandle bufferPin variables to a class-wide scope, because I was afraid either (or both) might be destroyed at some point by the garbage collector and thus the API functions would scew up. Since I changed that wavehdr.dwBufferLength is not an arbitary large number anymore, but 0. While it still does not work I am sure this is an improvement.
Daniel Baulig
+1  A: 

PInvoke.Net is THE place to go for finding PInvoke declarations. This page describes the waveInAddBuffer method and its C# equivalent, as well as links to WAVEHDR.

I had a look at various methods that you use but could not find anything helpful in your case. A difference with PInvoke.net's version and yours is that PInvoke uses the CharSet propery of StructLayout, but I guess it is not relevant.

A good book on the interoperability subject is: NET-and-COM

Timores
Thank you for the links, but sadly, as you already said, they did not help me in this issue. I tried toying around with the CharSet property without any success.
Daniel Baulig
A: 

Ok, I figured it out. All my code was basicly correct. However, I screwed up with the WAVEHDR structure. The waveIn* functions not only expect a reference to an WAVEHDR structure but also expect this structure to sustain until waveInHeaderUnprepare is called upon it. So the WAVEHDR structure will need to be created in an global or at least big enough context to sustain untl waveInHeaderUnprepare is called on it and propably needs to be pinned aswell using a GCHandle, so that it will not change it's position in memory (which afaik is not guaranteed in managed code). Here sis my updated and cleaned up code:

    private WinMM.WinMM.AudioRecordingDelegate waveIn;
    private IntPtr handle;
    private WinMM.WinMM.WAVEHDR header;
    private GCHandle headerPin;
    private GCHandle bufferPin;
    private byte[] buffer;
    private uint bufferLength;

    private void setupBuffer()
    {
        header.lpData = bufferPin.AddrOfPinnedObject();
        header.dwBufferLength = bufferLength;
        header.dwFlags = 0;
        header.dwBytesRecorded = 0;
        header.dwLoops = 0;
        header.dwUser = IntPtr.Zero;
        header.lpNext = IntPtr.Zero;
        header.reserved = IntPtr.Zero;

        int i = WinMM.WinMM.waveInPrepareHeader(handle, ref header, Convert.ToUInt32(Marshal.SizeOf(header)));
        if (i != WinMM.WinMM.MMSYSERR_NOERROR)
        {
            this.Text = "Error: waveInPrepare " + i.ToString();
            return;
        }
        i = WinMM.WinMM.waveInAddBuffer(handle, ref header, Convert.ToUInt32(Marshal.SizeOf(header)));
        if (i != WinMM.WinMM.MMSYSERR_NOERROR)
        {
            this.Text = "Error: waveInAddrBuffer";
            return;
        }
    }

    private void setupWaveIn()
    {
        handle = new IntPtr();
        WinMM.WinMM.WAVEFORMAT format;
        format.wFormatTag = WinMM.WinMM.WAVE_FORMAT_PCM;
        format.nChannels = 1;
        format.nSamplesPerSec = 8000;
        format.wBitsPerSample = 8;
        format.nBlockAlign = Convert.ToUInt16(format.nChannels * format.wBitsPerSample);
        format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
        bufferLength = format.nAvgBytesPerSec / 800;
        headerPin = GCHandle.Alloc(header, GCHandleType.Pinned);
        buffer = new byte[bufferLength];
        bufferPin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        format.cbSize = 0;

        int i = WinMM.WinMM.waveInOpen(out handle, WinMM.WinMM.WAVE_MAPPER, ref format, Marshal.GetFunctionPointerForDelegate(waveIn), IntPtr.Zero, WinMM.WinMM.CALLBACK_FUNCTION);
        if (i != WinMM.WinMM.MMSYSERR_NOERROR) 
        {
            this.Text = "Error: waveInOpen";
            return;
        }

        setupBuffer();

        WinMM.WinMM.waveInStart(handle);
    }

    private void callbackWaveIn(IntPtr deviceHandle, uint message, IntPtr instance, ref WinMM.WinMM.WAVEHDR wavehdr, IntPtr reserved2)
    {
        if (message == WinMM.WinMM.WIM_DATA)
            if (this.InvokeRequired)
                this.Invoke(waveIn, deviceHandle, message, instance, wavehdr, reserved2);
            else
            {
                if (wavehdr.dwBytesRecorded > 0)
                {
                    foreach (byte buf in buffer)
                    {
                        // do something cool with your byte stream
                    }
                }

                int i = WinMM.WinMM.waveInUnprepareHeader(deviceHandle, ref header, Convert.ToUInt32(Marshal.SizeOf(wavehdr)));
                if (i != WinMM.WinMM.MMSYSERR_NOERROR)
                {
                    this.Text = "Error: waveInUnprepareHeader " + i;
                }
                setupBuffer();
            }
    }

Thank you for your help. I hope someone can use the code I was able to come up with.

Daniel Baulig