When `RoGetAgileReference` is used with `AGILEREFERENCE_DELAYEDMARSHAL`, the returned `IAgileReference` also contains a reference to the apartment the object belongs to. Thus, when `IAgileReference::Resolve()` gets called, the object will get marshaled inside the owning apartment, not the caller's apartment.
This seems to be made possible by the `IContextCallback` interface. It provides a way to (ab)use the marshaller to execute arbitrary code inside a COM context, which is an additional bit of state nested inside apartments. An apartment can contain multiple COM contexts, and the standard marshaller will keep track of which context a marshaled interface is bound to. All method calls through that marshalled pointer will be wrapped around a context-switch, even when the caller is in the same apartment. Additionally, creating an instance of the class `CLSID_IContextCallback` allows us to create new contexts inside an apartment.
C++/WinRT makes use of `IContextCallback` to [implement support for C++ coroutines ](https://github.com/microsoft/cppwinrt/blob/ebace4abb8f48b6b9a612e8b84667e604..., et al) inside application STA code. Another use of COM contexts seems to be the UI code in WinRT applications, where certain objects can only be `Released`/destructed from the apartment they were created in.
(I was able to find another instance of `IContextCallback` usage in the wild [here](https://github.com/Open-Shell/Open-Shell-Menu/blob/4ebeadd949d05506428dacf36...), to get an `IAccessible` implementation that has apartment-affinity).
-- v2: ole32/tests: Add tests for IContextCallback::ContextCallback and CoDisconnectContext. combase/tests: Add tests for delayed marshaling of objects with apartment affinity.
From: Vibhav Pant vibhavp@gmail.com
When RoGetAgileReference is used with AGILEREFERENCE_DELAYEDMARSHAL, the returned IAgileReference also contains a reference to the apartment/context the object belongs to. Thus, when IAgileReference::Resolve() gets called, the object will get marshaled inside the owning apartment/context, not the caller's apartment. --- dlls/combase/tests/roapi.c | 228 +++++++++++++++++++++++++++++++++++++ include/roapi.h | 1 + 2 files changed, 229 insertions(+)
diff --git a/dlls/combase/tests/roapi.c b/dlls/combase/tests/roapi.c index e4100cb11bc..96e65b1f7b0 100644 --- a/dlls/combase/tests/roapi.c +++ b/dlls/combase/tests/roapi.c @@ -450,6 +450,167 @@ static const IUnknownVtbl unk_agile_vtbl = unk_Release };
+/* IUnknown implementation that has affinity to the STA it was created in. */ +struct unk_ctx_impl +{ + IUnknown IUnknown_iface; + BOOL todo; + UINT64 apt_id; /* Identifier of the apartment this object belongs to. */ + ULONG_PTR context; /* The COM context this object belongs to. */ + APTTYPE type; /* APTTYPE of the apartment this object belongs to. */ + DWORD thread_id; + GUID com_thread_id; + LONG ref; +}; + +static inline struct unk_ctx_impl *impl_unk_ctx_impl_from_IUnknown(IUnknown *iface) +{ + return CONTAINING_RECORD(iface, struct unk_ctx_impl, IUnknown_iface); +} + +/* If true, the method call is being made from the test thread. */ +static BOOL caller_test_thread; + +#define test_apartment_context(impl) test_apartment_context_(__LINE__, impl) +static void test_apartment_context_(int line, const struct unk_ctx_impl *impl) +{ + APTTYPEQUALIFIER qualifier; + ULONG_PTR cur_ctx; + UINT64 cur_id; + APTTYPE type; + HRESULT hr; + + hr = CoGetContextToken(&cur_ctx); + ok_(__FILE__, line)(hr == S_OK, "CoGetContextToken failed, got hr %#lx\n", hr); + /* As this object has apartment-affinity, its methods can only be called from the apartment and context it was + * created in. */ + todo_wine_if(impl->todo) ok_(__FILE__, line)(cur_ctx == impl->context, "got cur_ctx %#Ix != %#Ix\n", cur_ctx, impl->context); + + hr = CoGetApartmentType(&type, &qualifier); + ok_(__FILE__, line)(hr == S_OK, "CoGetApartmentType failed, got hr %#lx\n", hr); + todo_wine_if(impl->todo) ok_(__FILE__, line)(type == impl->type, "got type %d\n", type); + + hr = RoGetApartmentIdentifier(&cur_id); + ok_(__FILE__, line)(hr == S_OK, "RoGetApartmentIdentifier failed, got hr %#lx\n", hr); + ok_(__FILE__, line)(cur_id == impl->apt_id, "got cur_id %#I64x != %#I64x\n", cur_id, impl->apt_id); + + if (caller_test_thread) + { + DWORD cur_tid = GetCurrentThreadId(); + IUnknown *unk = (IUnknown *)cur_ctx; + GUID ctx_com_tid, cur_com_tid; + IComThreadingInfo *info; + ULONG count; + + hr = IUnknown_QueryInterface(unk, &IID_IComThreadingInfo, (void **)&info); + ok_(__FILE__, line)(hr == S_OK, "QueryInterface failed, got hr %#lx\n", hr); + + hr = IComThreadingInfo_GetCurrentLogicalThreadId(info, &ctx_com_tid); + ok_(__FILE__, line)(hr == S_OK, "GetCurrentLogicalThreadId failed, got hr %#lx\n", hr); + + /* However, the context object always returns the COM thread id of the *caller* thread. The marshaller likely + * calls SetCurrentLogicalThreadId on the context object before the method call. */ + hr = CoGetCurrentLogicalThreadId(&cur_com_tid); + ok_(__FILE__, line)(hr == S_OK, "CoGetCurrentLogicalThreadId failed, got hr %#lx\n", hr); + ok_(__FILE__, line)(IsEqualGUID(&cur_com_tid, &ctx_com_tid), "Got cur_com_tid %s != %s.\n", + debugstr_guid(&cur_com_tid), debugstr_guid(&ctx_com_tid)); + count = IComThreadingInfo_Release(info); + ok_(__FILE__, line)(!count, "Got count %lu.\n", count); + + /* If this object belongs to an STA, we should now be in the same thread that the object was created in. */ + if (impl->type == APTTYPE_STA || impl->type == APTTYPE_MAINSTA) + { + ok(cur_tid == impl->thread_id, "Got cur_tid %lu != %lu\n", cur_tid, impl->thread_id); + ok(!IsEqualGUID(&ctx_com_tid, &impl->com_thread_id), "Got cur_com_tid %s != %s\n", + debugstr_guid(&ctx_com_tid), debugstr_guid(&impl->com_thread_id)); + } + else + /* If this object belongs to a MTA, then the method call will be made from the same thread as that of the + * caller. */ + ok(cur_tid != impl->thread_id, "Got cur_tid %lu\n", cur_tid); + } +} + +static HRESULT WINAPI unk_ctx_impl_QueryInterface(IUnknown *iface, const GUID *iid, void **out) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + + if (winetest_debug > 1) + trace("(%p, %s, %p)\n", iface, debugstr_guid(iid), out); + + test_apartment_context(impl); + if (IsEqualGUID(iid, &IID_IUnknown) || IsEqualGUID(iid, &IID_IUnknown)) + { + IUnknown_AddRef((IUnknown *)(*out = &impl->IUnknown_iface)); + return S_OK; + } + + *out = NULL; + if (winetest_debug > 1) + trace("%s not implemeneted, returning E_NOINTERFACE\n", debugstr_guid(iid)); + return E_NOINTERFACE; +} + +static ULONG WINAPI unk_ctx_impl_AddRef(IUnknown *iface) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + + test_apartment_context(impl); + return InterlockedIncrement(&impl->ref); +} + +static ULONG WINAPI unk_ctx_impl_Release(IUnknown *iface) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + ULONG ref = InterlockedDecrement(&impl->ref); + + test_apartment_context(impl); + if (!ref) free(impl); + return ref; +} + +static const IUnknownVtbl unk_ctx_impl_IUnknown_vtbl = +{ + unk_ctx_impl_QueryInterface, + unk_ctx_impl_AddRef, + unk_ctx_impl_Release, +}; + +static HRESULT unk_ctx_impl_create(IUnknown **out) +{ + APTTYPEQUALIFIER qualifier; + struct unk_ctx_impl *impl; + HRESULT hr; + + if (!(impl = calloc(1, sizeof(*impl)))) return E_OUTOFMEMORY; + + impl->IUnknown_iface.lpVtbl = &unk_ctx_impl_IUnknown_vtbl; + if (FAILED(hr = CoGetContextToken(&impl->context))) + { + free(impl); + return hr; + } + if (FAILED(hr = CoGetApartmentType(&impl->type, &qualifier))) + { + free(impl); + return hr; + } + if (FAILED(hr = RoGetApartmentIdentifier(&impl->apt_id))) + { + free(impl); + return hr; + } + if (FAILED(hr = CoGetCurrentLogicalThreadId(&impl->com_thread_id))) + { + free(impl); + return hr; + } + impl->thread_id = GetCurrentThreadId(); + impl->ref = 1; + *out = &impl->IUnknown_iface; + return S_OK; +} + struct test_RoGetAgileReference_thread_param { enum AgileReferenceOptions option; @@ -499,12 +660,42 @@ static DWORD CALLBACK test_RoGetAgileReference_thread_proc(void *arg) return 0; }
+struct test_agile_resolve_context_params +{ + RO_INIT_TYPE from_type; + RO_INIT_TYPE to_type; + IAgileReference *ref; +}; + +static DWORD CALLBACK test_agile_resolve_context(void *arg) +{ + struct test_agile_resolve_context_params *params = arg; + IUnknown *unknown; + HRESULT hr; + + caller_test_thread = TRUE; + hr = RoInitialize(params->to_type); + ok(hr == S_OK, "got hr %#lx.\n", hr); + + winetest_push_context("from_type=%d, to_type=%d", params->from_type, params->to_type); + hr = IAgileReference_Resolve(params->ref, &IID_IUnknown, (void **)&unknown); + todo_wine_if(params->to_type == RO_INIT_MULTITHREADED) ok(hr == S_OK, "got hr %#lx\n", hr); + if (SUCCEEDED(hr)) + IUnknown_Release(unknown); + winetest_pop_context(); + + RoUninitialize(); + caller_test_thread = FALSE; + return 0; +} + static void test_RoGetAgileReference(void) { struct test_RoGetAgileReference_thread_param param; struct unk_impl unk_no_marshal_obj = {{&unk_no_marshal_vtbl}, 1}; struct unk_impl unk_obj = {{&unk_vtbl}, 1}; struct unk_impl unk_agile_obj = {{&unk_agile_vtbl}, 1}; + struct unk_ctx_impl *unk_ctx_impl; enum AgileReferenceOptions option; IAgileReference *agile_reference; RO_INIT_TYPE from_type, to_type; @@ -624,6 +815,43 @@ static void test_RoGetAgileReference(void) winetest_pop_context(); } } + + /* Tests specific to delayed marshaling */ + for (from_type = RO_INIT_SINGLETHREADED; from_type <= RO_INIT_MULTITHREADED; from_type++) + { + winetest_push_context("from_type=%d", from_type); + hr = RoInitialize(from_type); + ok(hr == S_OK, "got hr %#lx.\n", hr); + + hr = unk_ctx_impl_create(&unknown); + ok(hr == S_OK, "got hr %#lx\n", hr); + + unk_ctx_impl = impl_unk_ctx_impl_from_IUnknown(unknown); + hr = RoGetAgileReference(AGILEREFERENCE_DELAYEDMARSHAL, &IID_IUnknown, unknown, &agile_reference); + ok(hr == S_OK, "got hr %#lx\n", hr); + EXPECT_REF(unknown, 2); + + for (to_type = RO_INIT_SINGLETHREADED; to_type <= RO_INIT_MULTITHREADED; to_type++) + { + struct test_agile_resolve_context_params params = {from_type, to_type, agile_reference}; + + winetest_push_context("to_type=%d", to_type); + unk_ctx_impl->todo = TRUE; + thread = CreateThread(NULL, 0, test_agile_resolve_context, ¶ms, 0, NULL); + flush_events(); + ret = WaitForSingleObject(thread, INFINITE); + ok(!ret, "got ret %lu\n", ret); + CloseHandle(thread); + unk_ctx_impl->todo = FALSE; + winetest_pop_context(); + } + + IAgileReference_Release(agile_reference); + EXPECT_REF(unknown, 1); + IUnknown_Release(unknown); + RoUninitialize(); + winetest_pop_context(); + } }
static void test_RoGetErrorReportingFlags(void) diff --git a/include/roapi.h b/include/roapi.h index bcaa8e9fea0..18baf9e2017 100644 --- a/include/roapi.h +++ b/include/roapi.h @@ -46,6 +46,7 @@ HRESULT WINAPI RoActivateInstance(HSTRING classid, IInspectable **instance); HRESULT WINAPI RoGetActivationFactory(HSTRING classid, REFIID iid, void **class_factory); HRESULT WINAPI RoInitialize(RO_INIT_TYPE type); void WINAPI RoUninitialize(void); +HRESULT WINAPI RoGetApartmentIdentifier(UINT64 *identifier);
#ifdef __cplusplus }
From: Vibhav Pant vibhavp@gmail.com
--- dlls/ole32/tests/compobj.c | 283 +++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+)
diff --git a/dlls/ole32/tests/compobj.c b/dlls/ole32/tests/compobj.c index 2c4cc566f3a..811b7936530 100644 --- a/dlls/ole32/tests/compobj.c +++ b/dlls/ole32/tests/compobj.c @@ -72,6 +72,7 @@ DEFINE_EXPECT(PostUninitialize);
/* functions that are not present on all versions of Windows */ static HRESULT (WINAPI * pCoGetObjectContext)(REFIID riid, LPVOID *ppv); +static HRESULT (WINAPI *pCoDisconnectContext)(DWORD); static HRESULT (WINAPI * pCoSwitchCallContext)(IUnknown *pObject, IUnknown **ppOldObject); static HRESULT (WINAPI * pCoGetContextToken)(ULONG_PTR *token); static HRESULT (WINAPI * pCoGetApartmentType)(APTTYPE *type, APTTYPEQUALIFIER *qualifier); @@ -4065,6 +4066,7 @@ static void init_funcs(void) HMODULE hkernel32 = GetModuleHandleA("kernel32");
pCoGetObjectContext = (void*)GetProcAddress(hOle32, "CoGetObjectContext"); + pCoDisconnectContext = (void *)GetProcAddress(hOle32, "CoDisconnectContext"); pCoSwitchCallContext = (void*)GetProcAddress(hOle32, "CoSwitchCallContext"); pCoGetContextToken = (void*)GetProcAddress(hOle32, "CoGetContextToken"); pCoGetApartmentType = (void*)GetProcAddress(hOle32, "CoGetApartmentType"); @@ -4472,6 +4474,286 @@ static void test_oletlsdata(void) "Unexpected flags %#lx.\n", flags); }
+void flush_events(void) +{ + int diff = 200; + DWORD time; + MSG msg; + + time = GetTickCount() + diff; + while (diff > 0) + { + if (MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT) == WAIT_TIMEOUT) + break; + while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) + DispatchMessageA(&msg); + diff = time - GetTickCount(); + } +} + +/* A simple-enough interface that inherits from IUnknown, to test IContextCallback. */ +struct root_storage +{ + IRootStorage IRootStorage_iface; + ULONG_PTR token; + APTTYPE type; + APTTYPEQUALIFIER qualifier; + LONG ref; +}; + +static inline struct root_storage *impl_from_IRootStorage(IRootStorage *impl) +{ + return CONTAINING_RECORD(impl, struct root_storage, IRootStorage_iface); +} + +static HRESULT WINAPI root_storage_QueryInterface(IRootStorage *iface, const GUID *iid, void **out) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + + if (IsEqualGUID(iid, &IID_IUnknown) || IsEqualGUID(iid, &IID_IRootStorage)) + { + IRootStorage_AddRef((*out = &impl->IRootStorage_iface)); + return S_OK; + } + + *out = NULL; + return E_NOINTERFACE; +} + +static ULONG WINAPI root_storage_AddRef(IRootStorage *iface) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + return InterlockedIncrement(&impl->ref); +} + +static ULONG WINAPI root_storage_Release(IRootStorage *iface) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + ULONG ref = InterlockedDecrement(&impl->ref); + + if (!ref) free(impl); + return ref; +} + +static HRESULT WINAPI root_storage_SwitchToFile(IRootStorage *iface, LPOLESTR file) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + APTTYPEQUALIFIER qualifier; + ULONG_PTR token; + APTTYPE type; + HRESULT hr; + + hr = CoGetContextToken(&token); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* The call takes place inside the context the object was created in. */ + ok(token == impl->token, "got token %#Ix != %#Ix\n", token, impl->token); + + hr = CoGetApartmentType(&type, &qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + ok(type == impl->type, "got type %d != %d\n", type, impl->type); + ok(qualifier == impl->qualifier, "got qualifier %d != %d\n", qualifier, impl->qualifier); + + return S_OK; +} + +static const struct IRootStorageVtbl root_storage_vtbl = +{ + root_storage_QueryInterface, + root_storage_AddRef, + root_storage_Release, + root_storage_SwitchToFile +}; + +static HRESULT root_storage_create(IRootStorage **out, ULONG_PTR token, APTTYPE type, APTTYPEQUALIFIER qualifier) +{ + struct root_storage *impl; + + if (!(impl = calloc(1, sizeof(*impl)))) return E_OUTOFMEMORY; + impl->IRootStorage_iface.lpVtbl = &root_storage_vtbl; + impl->token = token; + impl->type = type; + impl->qualifier = qualifier; + impl->ref = 1; + *out = &impl->IRootStorage_iface; + return S_OK; +} + +struct context_callback_data +{ + APTTYPE from_type; + APTTYPEQUALIFIER from_qualifier; + ULONG_PTR from_token; + IStream *marshaled; +}; + +HRESULT CALLBACK create_object_callback(ComCallData *params) +{ + struct context_callback_data *data; + APTTYPEQUALIFIER qualifier; + ULONG_PTR cur_token; + IRootStorage *obj; + APTTYPE type; + ULONG count; + HRESULT hr; + + ok(!!params, "got params %p\n", params); + data = params->pUserDefined; + ok(!!data, "got data %p\n", data); + + hr = CoGetContextToken(&cur_token); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* We are now inside a new context, but the same apartment. */ + ok(cur_token != data->from_token, "got cur_token %#Ix\n", cur_token); + + hr = CoGetApartmentType(&type, &qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + ok(type == data->from_type, "got type %d != %d\n", type, data->from_type); + ok(qualifier == data->from_qualifier, "got qualifier %d != %d\n", qualifier, data->from_qualifier); + + hr = root_storage_create(&obj, cur_token, type, qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoMarshalInterThreadInterfaceInStream(&IID_IRootStorage, (IUnknown *)obj, &data->marshaled); + ok(hr == S_OK, "got hr %#lx\n", hr); + + count = IRootStorage_Release(obj); + ok(count == 4, "got count %lu\n", count); + + return S_OK; +} + +HRESULT CALLBACK disconnect_context_callback(ComCallData *data) +{ + return pCoDisconnectContext(100); +} + +struct test_marshaled_data +{ + IStream *marshaled; + COINIT from_type; + COINIT to_type; +}; + +DEFINE_GUID(CLSID_ContextSwitcher, 0x0000034e,0x0000,0x0000,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46); +static DWORD CALLBACK test_marshaled(void *param) +{ + struct test_marshaled_data *data = param; + WCHAR *file = wcsdup(L"foo"); + IStream *stream = data->marshaled; + IRootStorage *obj; + ULONG count; + HRESULT hr; + + winetest_push_context("from_type=%#x, to_type=%#x", data->from_type, data->to_type); + + hr = CoInitializeEx(NULL, data->to_type); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetInterfaceAndReleaseStream(stream, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == S_OK, "got hr %#lx\n", hr); + + count = IRootStorage_Release(obj); + ok(count == 0, "got count %lu\n", count); + + winetest_pop_context(); + + free(file); + return 0; +} + +static void test_IContextCallback(void) +{ + static const COINIT apt_types[] = {COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED}; + WCHAR *file = wcsdup(L"foo"); + HRESULT hr; + int i, j; + + for (i = 0; i < ARRAY_SIZE(apt_types); i++) + { + for (j = 0; j < ARRAY_SIZE(apt_types); j++) + { + struct test_marshaled_data data = {NULL, apt_types[i], apt_types[j]}; + struct context_callback_data callback_data = {0}; + ComCallData call_data = {0}; + IContextCallback *context; + IRootStorage *obj; + HANDLE thread; + DWORD ret; + + winetest_push_context("from_type=%#x, to_type=%#x", apt_types[i], apt_types[j]); + hr = CoInitializeEx(NULL, apt_types[i]); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetApartmentType(&callback_data.from_type, &callback_data.from_qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetContextToken(&callback_data.from_token); + ok(hr == S_OK, "got hr %#lx\n", hr); + + /* Create a new context inside this apartment. */ + hr = CoCreateInstance(&CLSID_ContextSwitcher, NULL, CLSCTX_INPROC_SERVER, &IID_IContextCallback, (void **)&context); + todo_wine ok(hr == S_OK, "got hr %#lx\n", hr); + if (FAILED(hr)) + { + CoUninitialize(); + winetest_pop_context(); + continue; + } + + /* Use ContextCallback to create and marshal and object bound to the new context. + * All methods in this object will be called inside the new context. */ + call_data.pUserDefined = &callback_data; + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + data.marshaled = callback_data.marshaled; + thread = CreateThread(NULL, 0, test_marshaled, &data, 0, NULL); + flush_events(); + ret = WaitForSingleObject(thread, 100); + ok(!ret, "got ret %lu\n", ret); + CloseHandle(thread); + + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + hr = CoGetInterfaceAndReleaseStream(callback_data.marshaled, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* Calls within the same apartment through the marshaled pointer will *also* be wrapped within a + * context-switch. */ + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == S_OK, "got hr %#lx\n", hr); + ret = IRootStorage_Release(obj); + ok(ret == 0, "got ret %lu\n", ret); + + if (pCoDisconnectContext) + { + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + hr = CoGetInterfaceAndReleaseStream(callback_data.marshaled, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* Disonnect the proxy from its context. */ + hr = IContextCallback_ContextCallback(context, disconnect_context_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* All calls should now return RPC_E_DISCONNECTED. */ + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == RPC_E_DISCONNECTED, "got hr %#lx\n", hr); + ret = IRootStorage_Release(obj); + ok(ret == 0, "got ret %lu\n", ret); + } + else + todo_wine win_skip("CoDisconnectContext not available\n"); + + ret = IContextCallback_Release(context); + ok(ret == 0, "got ret %lu\n", ret); + CoUninitialize(); + winetest_pop_context(); + } + } + + free(file); +} + START_TEST(compobj) { init_funcs(); @@ -4524,6 +4806,7 @@ START_TEST(compobj) test_mta_usage(); test_CoCreateInstanceFromApp(); test_call_cancellation(); + test_IContextCallback();
DeleteFileA( testlib ); }