Search code examples
c#marshallingvtable

Marhal non COM interface to unmanaged code


I've implemented this method in c#:

HRESULT CreateSourceVoice(
  [out]           IXAudio2SourceVoice **ppSourceVoice,
  [in]            const WAVEFORMATEX *pSourceFormat,
  [in]            UINT32 Flags = 0,
  [in]            float MaxFrequencyRatio = XAUDIO2_DEFAULT_FREQ_RATIO,
  [in, optional]  IXAudio2VoiceCallback *pCallback = NULL,
  [in, out]       const XAUDIO2_VOICE_SENDS *pSendList = NULL,
  [in, optional]  const XAUDIO2_EFFECT_CHAIN *pEffectChain = NULL
);

But I've got a problem with marshaling the 5th parameter. But first this is my implementation of the IXAudio2VoiceCallback interface:

[SuppressUnmanagedCodeSecurity]
public interface IXAudio2VoiceCallback
{
    void OnVoiceProcessingPassStart([In] Int32 bytesRequired);

    void OnVoiceProcessingPassEnd();

    void OnStreamEnd();

    void OnBufferStart([In] IntPtr bufferContextPtr);

    void OnBufferEnd([In] IntPtr bufferContextPtr);

    void OnLoopEnd([In] IntPtr bufferContextPtr);

    void OnVoiceError([In] IntPtr bufferContextPtr, [In] int error);
}

public class VoiceCallback : IXAudio2VoiceCallback
{
    //... events ...

    void IXAudio2VoiceCallback.OnVoiceProcessingPassStart(int bytesRequired)
    {
        if(ProcessingPassStart != null)
            ProcessingPassStart(bytesRequired);
    }

    void IXAudio2VoiceCallback.OnVoiceProcessingPassEnd()
    {
        if(ProcessingPassEnd != null)
            ProcessingPassEnd();
    }

    void IXAudio2VoiceCallback.OnStreamEnd()
    {
        if (StreamEnd != null)
            StreamEnd();
    }

    void IXAudio2VoiceCallback.OnBufferStart(IntPtr bufferContextPtr)
    {
        if(BufferStart != null)
            BufferStart(bufferContextPtr);
    }

    void IXAudio2VoiceCallback.OnBufferEnd(IntPtr bufferContextPtr)
    {
        if(BufferEnd != null)
            BufferEnd(bufferContextPtr);
    }

    void IXAudio2VoiceCallback.OnLoopEnd(IntPtr bufferContextPtr)
    {
        if(LoopEnd != null)
            LoopEnd(bufferContextPtr);
    }

    void IXAudio2VoiceCallback.OnVoiceError(IntPtr bufferContextPtr, int error)
    {
        if(VoiceError != null)
            VoiceError(bufferContextPtr, error);
    }
}

But now my actual problem: I can call CreateSourceVoice and it returns 0 (-> S_OK) but at any time, the callback should be called, the process just stops. There are no error messages (I've also checked the windows event log). Since I am using a quite similar concept as SharpDX does (see here), I've checked the SharpDX source. As far as I could see, SharpDX builds a vtable (see here).

But I am asking myself, whether it is possible to avoid building my own vtable? Isn't there a easier way?

I just would need to be able to get the vtable of the VoiceCallback-class.

The code for CreateSourceVoice looks like this:

public unsafe int CreateSourceVoiceNative(out IntPtr pSourceVoice, IntPtr sourceFormat, VoiceFlags flags, float maxFrequencyRatio, IXAudio2VoiceCallback voiceCallback, VoiceSends? sendList, EffectChain? effectChain)
{
    VoiceSends value0 = sendList.HasValue ? sendList.Value : default(VoiceSends);
    EffectChain value = effectChain.HasValue ? effectChain.Value : default(EffectChain);
    return calli(System.Int32(System.Void*,System.Void*,System.IntPtr,CSCore.XAudio2.VoiceFlags,System.Single,CSCore.XAudio2.IXAudio2VoiceCallback,System.Void*,System.Void*), this._basePtr, &pSourceVoice, sourceFormat, flags, maxFrequencyRatio, voiceCallback, sendList.HasValue ? ((void*)(&value0)) : ((void*)IntPtr.Zero), effectChain.HasValue ? ((void*)(&value)) : ((void*)IntPtr.Zero), *(*(IntPtr*)this._basePtr + (IntPtr)5 * (IntPtr)sizeof(void*)));
}

Solution

  • I've actually found a solution. The solution is to override the function pointers of the IUnknown interface in the vtalbe of the com object. I've written a quite ugly but working utils method which patches the vtable:

    private static readonly List<IntPtr> _patchedVtables = new List<IntPtr>(); 
    
    public unsafe static IntPtr GetComInterfaceForObjectWithAdjustedVtable(IntPtr ptr, int finalVtableLength, int replaceCount)
    {
        var pp = (IntPtr*) (void*) ptr;
        pp = (IntPtr*)pp[0];
    
        IntPtr z = new IntPtr((void*)pp);
    
        //since the same vtable applies to all com objects of the same type -> make sure to only patch it once
        if (_patchedVtables.Contains(z))
        {
            return ptr;
        }
    
        _patchedVtables.Add(z);
    
        for (int i = 0; i < finalVtableLength; i++)
        {
            IntPtr prev = pp[i];
    
            pp[i] = pp[i + replaceCount];
    
            IntPtr after = pp[i];
            //Console.WriteLine("{0} -> {1}", prev, after); //just for debugging
        }
        return ptr;
    }
    

    And just use it like this:

    p = Marshal.GetComInterfaceForObject(voiceCallback, typeof (IXAudio2VoiceCallback));
    p = Utils.Utils.GetComInterfaceForObjectWithAdjustedVtable(p, 7, 3);
    

    The second parameter (in this case 7) is the number of methods, the IXAudio2VoiceCallback contains and the third paramter (in this case 3) is the number of methods to override -> the number of methods of the IUnknown which is three (QueryInterface, AddRef, Release).


    Again, big thanks to xoofx.