ole32: Support registration-free COM in CoGetClassObject().
This commit is contained in:
parent
99ceb599bb
commit
af93b53082
|
@ -90,6 +90,54 @@ static CRITICAL_SECTION_DEBUG critsect_debug =
|
|||
};
|
||||
static CRITICAL_SECTION csApartment = { &critsect_debug, -1, 0, 0, 0, 0 };
|
||||
|
||||
enum comclass_threadingmodel
|
||||
{
|
||||
ThreadingModel_Apartment = 1,
|
||||
ThreadingModel_Free = 2,
|
||||
ThreadingModel_No = 3,
|
||||
ThreadingModel_Both = 4,
|
||||
ThreadingModel_Neutral = 5
|
||||
};
|
||||
|
||||
struct comclassredirect_data
|
||||
{
|
||||
ULONG size;
|
||||
BYTE res;
|
||||
BYTE miscmask;
|
||||
BYTE res1[2];
|
||||
DWORD model;
|
||||
GUID clsid;
|
||||
GUID alias;
|
||||
GUID clsid2;
|
||||
GUID tlbid;
|
||||
ULONG name_len;
|
||||
ULONG name_offset;
|
||||
ULONG progid_len;
|
||||
ULONG progid_offset;
|
||||
ULONG clrdata_len;
|
||||
ULONG clrdata_offset;
|
||||
DWORD miscstatus;
|
||||
DWORD miscstatuscontent;
|
||||
DWORD miscstatusthumbnail;
|
||||
DWORD miscstatusicon;
|
||||
DWORD miscstatusdocprint;
|
||||
};
|
||||
|
||||
struct class_reg_data
|
||||
{
|
||||
union
|
||||
{
|
||||
struct
|
||||
{
|
||||
struct comclassredirect_data *data;
|
||||
void *section;
|
||||
HANDLE hactctx;
|
||||
} actctx;
|
||||
HKEY hkey;
|
||||
} u;
|
||||
BOOL hkey;
|
||||
};
|
||||
|
||||
struct registered_psclsid
|
||||
{
|
||||
struct list entry;
|
||||
|
@ -1227,14 +1275,17 @@ static HRESULT apartment_getclassobject(struct apartment *apt, LPCWSTR dllpath,
|
|||
*
|
||||
* Reads a registry value and expands it when necessary
|
||||
*/
|
||||
static DWORD COM_RegReadPath(HKEY hkeyroot, WCHAR * dst, DWORD dstlen)
|
||||
static DWORD COM_RegReadPath(const struct class_reg_data *regdata, WCHAR *dst, DWORD dstlen)
|
||||
{
|
||||
DWORD ret;
|
||||
DWORD ret;
|
||||
|
||||
if (regdata->hkey)
|
||||
{
|
||||
DWORD keytype;
|
||||
WCHAR src[MAX_PATH];
|
||||
DWORD dwLength = dstlen * sizeof(WCHAR);
|
||||
|
||||
if( (ret = RegQueryValueExW(hkeyroot, NULL, NULL, &keytype, (LPBYTE)src, &dwLength)) == ERROR_SUCCESS ) {
|
||||
if( (ret = RegQueryValueExW(regdata->u.hkey, NULL, NULL, &keytype, (BYTE*)src, &dwLength)) == ERROR_SUCCESS ) {
|
||||
if (keytype == REG_EXPAND_SZ) {
|
||||
if (dstlen <= ExpandEnvironmentStringsW(src, dst, dstlen)) ret = ERROR_MORE_DATA;
|
||||
} else {
|
||||
|
@ -1252,11 +1303,24 @@ static DWORD COM_RegReadPath(HKEY hkeyroot, WCHAR * dst, DWORD dstlen)
|
|||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
ULONG_PTR cookie;
|
||||
WCHAR *nameW;
|
||||
|
||||
*dst = 0;
|
||||
nameW = (WCHAR*)((BYTE*)regdata->u.actctx.section + regdata->u.actctx.data->name_offset);
|
||||
ActivateActCtx(regdata->u.actctx.hactctx, &cookie);
|
||||
ret = SearchPathW(NULL, nameW, NULL, dstlen, dst, NULL);
|
||||
DeactivateActCtx(0, cookie);
|
||||
return !*dst;
|
||||
}
|
||||
}
|
||||
|
||||
struct host_object_params
|
||||
{
|
||||
HKEY hkeydll;
|
||||
struct class_reg_data regdata;
|
||||
CLSID clsid; /* clsid of object to marshal */
|
||||
IID iid; /* interface to marshal */
|
||||
HANDLE event; /* event signalling when ready for multi-threaded case */
|
||||
|
@ -1275,7 +1339,7 @@ static HRESULT apartment_hostobject(struct apartment *apt,
|
|||
|
||||
TRACE("clsid %s, iid %s\n", debugstr_guid(¶ms->clsid), debugstr_guid(¶ms->iid));
|
||||
|
||||
if (COM_RegReadPath(params->hkeydll, dllpath, ARRAYSIZE(dllpath)) != ERROR_SUCCESS)
|
||||
if (COM_RegReadPath(¶ms->regdata, dllpath, ARRAYSIZE(dllpath)) != ERROR_SUCCESS)
|
||||
{
|
||||
/* failure: CLSID is not found in registry */
|
||||
WARN("class %s not registered inproc\n", debugstr_guid(¶ms->clsid));
|
||||
|
@ -1372,7 +1436,7 @@ static DWORD CALLBACK apartment_hostobject_thread(LPVOID p)
|
|||
* caller of this function */
|
||||
static HRESULT apartment_hostobject_in_hostapt(
|
||||
struct apartment *apt, BOOL multi_threaded, BOOL main_apartment,
|
||||
HKEY hkeydll, REFCLSID rclsid, REFIID riid, void **ppv)
|
||||
const struct class_reg_data *regdata, REFCLSID rclsid, REFIID riid, void **ppv)
|
||||
{
|
||||
struct host_object_params params;
|
||||
HWND apartment_hwnd = NULL;
|
||||
|
@ -1442,7 +1506,7 @@ static HRESULT apartment_hostobject_in_hostapt(
|
|||
}
|
||||
}
|
||||
|
||||
params.hkeydll = hkeydll;
|
||||
params.regdata = *regdata;
|
||||
params.clsid = *rclsid;
|
||||
params.iid = *riid;
|
||||
hr = CreateStreamOnHGlobal(NULL, TRUE, ¶ms.stream);
|
||||
|
@ -2586,19 +2650,36 @@ HRESULT WINAPI CoRegisterClassObject(
|
|||
return S_OK;
|
||||
}
|
||||
|
||||
static void get_threading_model(HKEY key, LPWSTR value, DWORD len)
|
||||
static enum comclass_threadingmodel get_threading_model(const struct class_reg_data *data)
|
||||
{
|
||||
static const WCHAR wszThreadingModel[] = {'T','h','r','e','a','d','i','n','g','M','o','d','e','l',0};
|
||||
DWORD keytype;
|
||||
DWORD ret;
|
||||
DWORD dwLength = len * sizeof(WCHAR);
|
||||
if (data->hkey)
|
||||
{
|
||||
static const WCHAR wszThreadingModel[] = {'T','h','r','e','a','d','i','n','g','M','o','d','e','l',0};
|
||||
static const WCHAR wszApartment[] = {'A','p','a','r','t','m','e','n','t',0};
|
||||
static const WCHAR wszFree[] = {'F','r','e','e',0};
|
||||
static const WCHAR wszBoth[] = {'B','o','t','h',0};
|
||||
WCHAR threading_model[10 /* strlenW(L"apartment")+1 */];
|
||||
DWORD dwLength = sizeof(threading_model);
|
||||
DWORD keytype;
|
||||
DWORD ret;
|
||||
|
||||
ret = RegQueryValueExW(key, wszThreadingModel, NULL, &keytype, (LPBYTE)value, &dwLength);
|
||||
if ((ret != ERROR_SUCCESS) || (keytype != REG_SZ))
|
||||
value[0] = '\0';
|
||||
ret = RegQueryValueExW(data->u.hkey, wszThreadingModel, NULL, &keytype, (BYTE*)threading_model, &dwLength);
|
||||
if ((ret != ERROR_SUCCESS) || (keytype != REG_SZ))
|
||||
threading_model[0] = '\0';
|
||||
|
||||
if (!strcmpiW(threading_model, wszApartment)) return ThreadingModel_Apartment;
|
||||
if (!strcmpiW(threading_model, wszFree)) return ThreadingModel_Free;
|
||||
if (!strcmpiW(threading_model, wszBoth)) return ThreadingModel_Both;
|
||||
|
||||
/* there's not specific handling for this case */
|
||||
if (threading_model[0]) return ThreadingModel_Neutral;
|
||||
return ThreadingModel_No;
|
||||
}
|
||||
else
|
||||
return data->u.actctx.data->model;
|
||||
}
|
||||
|
||||
static HRESULT get_inproc_class_object(APARTMENT *apt, HKEY hkeydll,
|
||||
static HRESULT get_inproc_class_object(APARTMENT *apt, const struct class_reg_data *regdata,
|
||||
REFCLSID rclsid, REFIID riid,
|
||||
BOOL hostifnecessary, void **ppv)
|
||||
{
|
||||
|
@ -2607,37 +2688,30 @@ static HRESULT get_inproc_class_object(APARTMENT *apt, HKEY hkeydll,
|
|||
|
||||
if (hostifnecessary)
|
||||
{
|
||||
static const WCHAR wszApartment[] = {'A','p','a','r','t','m','e','n','t',0};
|
||||
static const WCHAR wszFree[] = {'F','r','e','e',0};
|
||||
static const WCHAR wszBoth[] = {'B','o','t','h',0};
|
||||
WCHAR threading_model[10 /* strlenW(L"apartment")+1 */];
|
||||
enum comclass_threadingmodel model = get_threading_model(regdata);
|
||||
|
||||
get_threading_model(hkeydll, threading_model, ARRAYSIZE(threading_model));
|
||||
/* "Apartment" */
|
||||
if (!strcmpiW(threading_model, wszApartment))
|
||||
if (model == ThreadingModel_Apartment)
|
||||
{
|
||||
apartment_threaded = TRUE;
|
||||
if (apt->multi_threaded)
|
||||
return apartment_hostobject_in_hostapt(apt, FALSE, FALSE, hkeydll, rclsid, riid, ppv);
|
||||
return apartment_hostobject_in_hostapt(apt, FALSE, FALSE, regdata, rclsid, riid, ppv);
|
||||
}
|
||||
/* "Free" */
|
||||
else if (!strcmpiW(threading_model, wszFree))
|
||||
else if (model == ThreadingModel_Free)
|
||||
{
|
||||
apartment_threaded = FALSE;
|
||||
if (!apt->multi_threaded)
|
||||
return apartment_hostobject_in_hostapt(apt, TRUE, FALSE, hkeydll, rclsid, riid, ppv);
|
||||
return apartment_hostobject_in_hostapt(apt, TRUE, FALSE, regdata, rclsid, riid, ppv);
|
||||
}
|
||||
/* everything except "Apartment", "Free" and "Both" */
|
||||
else if (strcmpiW(threading_model, wszBoth))
|
||||
else if (model != ThreadingModel_Both)
|
||||
{
|
||||
apartment_threaded = TRUE;
|
||||
/* everything else is main-threaded */
|
||||
if (threading_model[0])
|
||||
FIXME("unrecognised threading model %s for object %s, should be main-threaded?\n",
|
||||
debugstr_w(threading_model), debugstr_guid(rclsid));
|
||||
if (model != ThreadingModel_No)
|
||||
FIXME("unrecognised threading model %d for object %s, should be main-threaded?\n", model, debugstr_guid(rclsid));
|
||||
|
||||
if (apt->multi_threaded || !apt->main)
|
||||
return apartment_hostobject_in_hostapt(apt, FALSE, TRUE, hkeydll, rclsid, riid, ppv);
|
||||
return apartment_hostobject_in_hostapt(apt, FALSE, TRUE, regdata, rclsid, riid, ppv);
|
||||
}
|
||||
else
|
||||
apartment_threaded = FALSE;
|
||||
|
@ -2645,7 +2719,7 @@ static HRESULT get_inproc_class_object(APARTMENT *apt, HKEY hkeydll,
|
|||
else
|
||||
apartment_threaded = !apt->multi_threaded;
|
||||
|
||||
if (COM_RegReadPath(hkeydll, dllpath, ARRAYSIZE(dllpath)) != ERROR_SUCCESS)
|
||||
if (COM_RegReadPath(regdata, dllpath, ARRAYSIZE(dllpath)) != ERROR_SUCCESS)
|
||||
{
|
||||
/* failure: CLSID is not found in registry */
|
||||
WARN("class %s not registered inproc\n", debugstr_guid(rclsid));
|
||||
|
@ -2686,7 +2760,8 @@ HRESULT WINAPI CoGetClassObject(
|
|||
REFCLSID rclsid, DWORD dwClsContext, COSERVERINFO *pServerInfo,
|
||||
REFIID iid, LPVOID *ppv)
|
||||
{
|
||||
LPUNKNOWN regClassObject;
|
||||
struct class_reg_data clsreg;
|
||||
IUnknown *regClassObject;
|
||||
HRESULT hres = E_UNEXPECTED;
|
||||
APARTMENT *apt;
|
||||
BOOL release_apt = FALSE;
|
||||
|
@ -2713,6 +2788,37 @@ HRESULT WINAPI CoGetClassObject(
|
|||
debugstr_w(pServerInfo->pwszName), pServerInfo->pAuthInfo);
|
||||
}
|
||||
|
||||
if (CLSCTX_INPROC_SERVER & dwClsContext)
|
||||
{
|
||||
if (IsEqualCLSID(rclsid, &CLSID_InProcFreeMarshaler))
|
||||
{
|
||||
if (release_apt) apartment_release(apt);
|
||||
return FTMarshalCF_Create(iid, ppv);
|
||||
}
|
||||
}
|
||||
|
||||
if (CLSCTX_INPROC & dwClsContext)
|
||||
{
|
||||
ACTCTX_SECTION_KEYED_DATA data;
|
||||
|
||||
data.cbSize = sizeof(data);
|
||||
/* search activation context first */
|
||||
if (FindActCtxSectionGuid(FIND_ACTCTX_SECTION_KEY_RETURN_HACTCTX, NULL,
|
||||
ACTIVATION_CONTEXT_SECTION_COM_SERVER_REDIRECTION,
|
||||
rclsid, &data))
|
||||
{
|
||||
clsreg.u.actctx.hactctx = data.hActCtx;
|
||||
clsreg.u.actctx.data = data.lpData;
|
||||
clsreg.u.actctx.section = data.lpSectionBase;
|
||||
clsreg.hkey = FALSE;
|
||||
|
||||
hres = get_inproc_class_object(apt, &clsreg, rclsid, iid, !(dwClsContext & WINE_CLSCTX_DONT_HOST), ppv);
|
||||
ReleaseActCtx(data.hActCtx);
|
||||
if (release_apt) apartment_release(apt);
|
||||
return hres;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* First, try and see if we can't match the class ID with one of the
|
||||
* registered classes.
|
||||
|
@ -2739,12 +2845,6 @@ HRESULT WINAPI CoGetClassObject(
|
|||
static const WCHAR wszInprocServer32[] = {'I','n','p','r','o','c','S','e','r','v','e','r','3','2',0};
|
||||
HKEY hkey;
|
||||
|
||||
if (IsEqualCLSID(rclsid, &CLSID_InProcFreeMarshaler))
|
||||
{
|
||||
if (release_apt) apartment_release(apt);
|
||||
return FTMarshalCF_Create(iid, ppv);
|
||||
}
|
||||
|
||||
hres = COM_OpenKeyForCLSID(rclsid, wszInprocServer32, KEY_READ, &hkey);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
|
@ -2759,8 +2859,10 @@ HRESULT WINAPI CoGetClassObject(
|
|||
|
||||
if (SUCCEEDED(hres))
|
||||
{
|
||||
hres = get_inproc_class_object(apt, hkey, rclsid, iid,
|
||||
!(dwClsContext & WINE_CLSCTX_DONT_HOST), ppv);
|
||||
clsreg.u.hkey = hkey;
|
||||
clsreg.hkey = TRUE;
|
||||
|
||||
hres = get_inproc_class_object(apt, &clsreg, rclsid, iid, !(dwClsContext & WINE_CLSCTX_DONT_HOST), ppv);
|
||||
RegCloseKey(hkey);
|
||||
}
|
||||
|
||||
|
@ -2793,8 +2895,10 @@ HRESULT WINAPI CoGetClassObject(
|
|||
|
||||
if (SUCCEEDED(hres))
|
||||
{
|
||||
hres = get_inproc_class_object(apt, hkey, rclsid, iid,
|
||||
!(dwClsContext & WINE_CLSCTX_DONT_HOST), ppv);
|
||||
clsreg.u.hkey = hkey;
|
||||
clsreg.hkey = TRUE;
|
||||
|
||||
hres = get_inproc_class_object(apt, &clsreg, rclsid, iid, !(dwClsContext & WINE_CLSCTX_DONT_HOST), ppv);
|
||||
RegCloseKey(hkey);
|
||||
}
|
||||
|
||||
|
@ -4557,9 +4661,13 @@ HRESULT Handler_DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv)
|
|||
hres = COM_OpenKeyForCLSID(rclsid, wszInprocHandler32, KEY_READ, &hkey);
|
||||
if (SUCCEEDED(hres))
|
||||
{
|
||||
struct class_reg_data regdata;
|
||||
WCHAR dllpath[MAX_PATH+1];
|
||||
|
||||
if (COM_RegReadPath(hkey, dllpath, ARRAYSIZE(dllpath)) == ERROR_SUCCESS)
|
||||
regdata.u.hkey = hkey;
|
||||
regdata.hkey = TRUE;
|
||||
|
||||
if (COM_RegReadPath(®data, dllpath, ARRAYSIZE(dllpath)) == ERROR_SUCCESS)
|
||||
{
|
||||
static const WCHAR wszOle32[] = {'o','l','e','3','2','.','d','l','l',0};
|
||||
if (!strcmpiW(dllpath, wszOle32))
|
||||
|
|
|
@ -63,6 +63,8 @@ static const GUID IID_Testiface5 = { 0x62222222, 0x1234, 0x1234, { 0x12, 0x34, 0
|
|||
static const GUID IID_Testiface6 = { 0x72222222, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } };
|
||||
static const GUID IID_TestPS = { 0x66666666, 0x8888, 0x7777, { 0x66, 0x66, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 } };
|
||||
|
||||
DEFINE_GUID(CLSID_InProcFreeMarshaler, 0x0000033a,0x0000,0x0000,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46);
|
||||
|
||||
static WCHAR stdfont[] = {'S','t','d','F','o','n','t',0};
|
||||
static const WCHAR wszNonExistent[] = {'N','o','n','E','x','i','s','t','e','n','t',0};
|
||||
static WCHAR wszCLSID_StdFont[] =
|
||||
|
@ -225,6 +227,14 @@ static const char actctx_manifest[] =
|
|||
"<assemblyIdentity version=\"1.2.3.4\" name=\"Wine.Test\" type=\"win32\""
|
||||
" publicKeyToken=\"6595b6414666f1df\" />"
|
||||
"<file name=\"testlib.dll\">"
|
||||
" <comClass"
|
||||
" clsid=\"{0000033a-0000-0000-c000-000000000046}\""
|
||||
" progid=\"FTMarshal\""
|
||||
" />"
|
||||
" <comClass"
|
||||
" clsid=\"{5201163f-8164-4fd0-a1a2-5d5a3654d3bd}\""
|
||||
" progid=\"WineOOPTest\""
|
||||
" />"
|
||||
" <comClass description=\"Test com class\""
|
||||
" clsid=\"{12345678-1234-1234-1234-56789abcdef0}\""
|
||||
" progid=\"ProgId.ProgId\""
|
||||
|
@ -579,8 +589,9 @@ static void test_CoCreateInstance(void)
|
|||
static void test_CoGetClassObject(void)
|
||||
{
|
||||
HRESULT hr;
|
||||
HANDLE thread;
|
||||
HANDLE thread, handle;
|
||||
DWORD tid, exitcode;
|
||||
ULONG_PTR cookie;
|
||||
IUnknown *pUnk;
|
||||
struct info info;
|
||||
REFCLSID rclsid = &CLSID_InternetZoneManager;
|
||||
|
@ -660,6 +671,22 @@ static void test_CoGetClassObject(void)
|
|||
if (hr == S_OK) IUnknown_Release(pUnk);
|
||||
RegCloseKey(hkey);
|
||||
}
|
||||
|
||||
hr = CoGetClassObject(&CLSID_InProcFreeMarshaler, CLSCTX_INPROC_SERVER, NULL, &IID_IUnknown, (void **)&pUnk);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
IUnknown_Release(pUnk);
|
||||
|
||||
/* context redefines FreeMarshaler CLSID */
|
||||
if ((handle = activate_context(actctx_manifest, &cookie)))
|
||||
{
|
||||
hr = CoGetClassObject(&CLSID_InProcFreeMarshaler, CLSCTX_INPROC_SERVER, NULL, &IID_IUnknown, (void **)&pUnk);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
IUnknown_Release(pUnk);
|
||||
|
||||
pDeactivateActCtx(0, cookie);
|
||||
pReleaseActCtx(handle);
|
||||
}
|
||||
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
|
@ -1147,6 +1174,8 @@ static void test_CoMarshalInterThreadInterfaceInStream(void)
|
|||
|
||||
static void test_CoRegisterClassObject(void)
|
||||
{
|
||||
ULONG_PTR ctxcookie;
|
||||
HANDLE handle;
|
||||
DWORD cookie;
|
||||
HRESULT hr;
|
||||
IClassFactory *pcf;
|
||||
|
@ -1222,6 +1251,33 @@ static void test_CoRegisterClassObject(void)
|
|||
if (0)
|
||||
CoRevokeClassObject(cookie);
|
||||
|
||||
/* test that object is accessible */
|
||||
hr = CoRegisterClassObject(&CLSID_WineOOPTest, (IUnknown *)&Test_ClassFactory, CLSCTX_INPROC_SERVER,
|
||||
REGCLS_MULTIPLEUSE, &cookie);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
|
||||
hr = CoGetClassObject(&CLSID_WineOOPTest, CLSCTX_INPROC_SERVER, NULL, &IID_IClassFactory, (void**)&pcf);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
IClassFactory_Release(pcf);
|
||||
|
||||
/* context now contains CLSID_WineOOPTest, test if registered one could still be used */
|
||||
if ((handle = activate_context(actctx_manifest, &ctxcookie)))
|
||||
{
|
||||
hr = CoGetClassObject(&CLSID_WineOOPTest, CLSCTX_INPROC_SERVER, NULL, &IID_IClassFactory, (void**)&pcf);
|
||||
todo_wine
|
||||
ok(hr == HRESULT_FROM_WIN32(ERROR_MOD_NOT_FOUND), "got 0x%08x\n", hr);
|
||||
|
||||
pDeactivateActCtx(0, ctxcookie);
|
||||
pReleaseActCtx(handle);
|
||||
}
|
||||
|
||||
hr = CoGetClassObject(&CLSID_WineOOPTest, CLSCTX_INPROC_SERVER, NULL, &IID_IClassFactory, (void**)&pcf);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
IClassFactory_Release(pcf);
|
||||
|
||||
hr = CoRevokeClassObject(cookie);
|
||||
ok(hr == S_OK, "got 0x%08x\n", hr);
|
||||
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
|
@ -1878,6 +1934,9 @@ START_TEST(compobj)
|
|||
return;
|
||||
}
|
||||
|
||||
if (!pCreateActCtxW)
|
||||
win_skip("Activation contexts are not supported, some tests will be skipped.\n");
|
||||
|
||||
test_ProgIDFromCLSID();
|
||||
test_CLSIDFromProgID();
|
||||
test_CLSIDFromString();
|
||||
|
|
Loading…
Reference in New Issue