microsoft / cppwinrt

C++/WinRT
MIT License
1.61k stars 232 forks source link

Bug: co_await apartment_context crashs with ''CoInitialize has not been called.'' #1415

Closed heroboy closed 2 months ago

heroboy commented 2 months ago

Version

C++/WinRT v2.0.220110.5

Summary

No response

Reproducible example


winrt::fire_and_forget foo()
{
    co_await winrt::Windows::Storage::StorageFolder::GetFolderFromPathAsync(L"c:\\");
}

winrt::fire_and_forget bar()
{
    winrt::apartment_context ctx;
    co_await winrt::resume_background();
    co_await ctx;
}

int main() {

    winrt::init_apartment(winrt::apartment_type::single_threaded);
    //foo();
    bar();

    MSG msg;
    while (GetMessage(&msg, 0, 0, 0))
    {
        DispatchMessage(&msg);
    }
    return 0;
}

Expected behavior

no crash.

Actual behavior

crash at co_await ctx; with 'CoInitialize has not been called.'

Additional comments

Uncomment the foo(), await any async windows runtime function, and later co_await ctx will no longer crash. Or call winrt::init_apartment() after winrt::resume_background()

dmachaj commented 2 months ago

This example program does not initialize the MTA in the process. The background tasks therefore run outside the MTA and the "you didn't initialize COM" error is raised.

The call to foo works around this issue because the implementation of StorageFolder::GetFolderFromPathAsync has a side effect of initializing the MTA in the process.

I think the appropriate fix would be to have your code initialize the MTA before it tries to use it. One way that can be done is by calling CoIncrementMTAUsage. Simply call that before the call to bar and the crash is avoided.

heroboy commented 2 months ago

Thank you. I had never think apartment is a property of process. I tested:

std::thread{ []() {
    winrt::init_apartment();
    ::Sleep(10000);
} }.detach();

std::thread{ []() {
    auto type = winrt::impl::get_apartment_type();
    printf("%d\n",type.first);
} }.join();

I think once a thread is initialized as MTA. All thread not initialized will be considered as MTA.

heroboy commented 2 months ago

But I find that initialized with single threaded will also make the ContextCallback success.

void test()
{
    winrt::com_ptr<IContextCallback> ctx;
    CoGetObjectContext(IID_IContextCallback, ctx.put_void());
    assert(ctx);

    TrySubmitThreadpoolCallback([](PTP_CALLBACK_INSTANCE pci, PVOID p) {

        auto x = winrt::impl::get_apartment_type();
        winrt::com_ptr<IContextCallback> ctx;
        *ctx.put_void() = p;
        winrt::init_apartment(winrt::apartment_type::single_threaded);
        ComCallData data;
        HRESULT hr = ctx->ContextCallback([](ComCallData*) {
            return S_OK;
        }, &data, IID_ICallbackWithNoReentrancyToApplicationSTA, 5, nullptr);
        printf("[%d]hr=%d\n", GetCurrentThreadId(), hr);

    }, ctx.detach(), 0);
}