Skip to content

HRESULT E_FAIL returned by native method is overwritten by wrong managed exception instead of COMException #116917

Open
@m-celikba

Description

@m-celikba

Description

I have 2 COM interfaces, one is implemented in native and one in managed (using source generated COM). The native implementation calls the managed implementation.
When the managed implementation throws InvalidOperationException, it is correctly translated to an HRESULT value in native code; The native code then deals with the error and returns E_FAIL;

I am testing this and the testing code in c# should capture COMException (because E_FAIL translats to that) but - and that is the bug here - it is translated to InvalidOperationException.

I don't see this documented in as a limitation so seems like a bug to me.

Reproduction Steps

//c++ code

struct __declspec(uuid("...")) __declspec(novtable) IRunnable : IUnknown
{
    virtual HRESULT STDCALL Run();
};

struct __declspec(uuid("...")) __declspec(novtable) IRunnableCaller : IUnknown
{
    virtual HRESULT STDCALL CallRunnable(IRunnable *pRunnable) = 0;
};

const GUID IID_IRunnable = {...}
const GUID IID_IRunnableCaller = {...}

class RunnableCaller : public IRunnableCaller
{
public:
    // IUnknown
    HRESULT STDCALL QueryInterface(REFIID riid, void **ppvObject) override
    {
        if (!ppvObject) return E_POINTER;

        if (InlineIsEqualGUID(riid, IID_IUnknown))
        {
            *ppvObject = static_cast<IUnknown *>(this);
            this->AddRef();
            return S_OK;
        }

        if (InlineIsEqualGUID(riid, IID_IRunnableCaller))
        {
            *ppvObject = this;
            this->AddRef();
            return S_OK;
        }

        return E_NOINTERFACE;
    }

    ULONG32 STDCALL AddRef() override
    {
        return ++m_refCount;
    }

    ULONG32 STDCALL Release() override
    {
        if (m_refCount > 0 && --m_refCount < 1)
        {
            delete this;
        }
        return m_refCount;
    }

    // IRunnableCaller
    HRESULT STDCALL CallRunnable(IRunnable *pRunnable) override
    {
        if (!pRunnable) return E_POINTER;
        const auto hr = pRunnable->Run();
        if (FAILED(hr)) return E_FAIL;
        return S_OK;
    }

private:
    ULONG32 m_refCount{ 0 };
};

extern "C" DOTNETHOST_TESTS_NATIVE_LIB_DLLEXPORT IUnknown * STDCALL CreateRunnableCaller()
{
    const auto pInst = new RunnableCaller();
    const auto ppUnk = static_cast<IUnknown*>(pInst);
    pInst->AddRef();
    return pInst;
}

// c# code

[GeneratedComInterface, Guid("...")]
internal partial interface IRunnableCaller
{
    void CallRunnable(IRunnable runnable);
}

[GeneratedComInterface, Guid("382E8E43-B6D6-44B6-AE1F-D178E17D755E")]
public partial interface IRunnable
{
    void Run();
}

[GeneratedComClass, Guid("C2063F0D-92EA-410C-9AA2-9D08AF79D623")]
public unsafe partial class Runnable : IRunnable
{
    public void Run()
    {
        throw new InvalidOperationException();
    }
}

internal static class RunnableCallerFactory
{
    [DllImport("RunnableCaller.dll")] public static extern IntPtr CreateRunnableCaller();
}


[Test]
public unsafe void PassingInterfaceReturningFailureWorks()
{
    var ptr = RunnableCallerFactory.CreateRunnableCaller();
    var obj = ComInterfaceMarshaller<IRunnableCaller>.ConvertToManaged(ptr.ToPointer());
    Assert.That(obj, Is.Not.Null);

    var runnable = new Runnable();
    Assert.Throws<COMException>(() => obj.CallRunnable(runnable)); // expected COMException but was InvalidOperationException
}

Expected behavior

E_FAIL should be translated to COMException regardless of whether the runtime caught other managed exception previously.

Assert.Throws(() => obj.CallRunnable(runnable)); should succeed

Actual behavior

Assert.Throws(() => obj.CallRunnable(runnable)); fails because InvalidOperationException is captured

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

Vaguely looking at the runtime code.. IErrorInfo might be the culprit ?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions