[PATCH v2 0/2] MR10565: shcore: Implement Set/GetCurrentProcessExplicitAppUserModelID.
Wine Bug: https://bugs.winehq.org/show_bug.cgi?id=59625 Set/GetCurrentProcessExplicitAppUserModelID were stubs. Set returned S_OK but discarded the value, Get always returned NULL with E_NOTIMPL. This broke .NET WPF applications that set an AppUserModelID during startup and later retrieve it, causing "String argument cannot be null or empty" crashes. Implement both using a process-wide variable protected by a critical section. Callers receive CoTaskMemAlloc'd copies per the API contract. -- v2: shcore/tests: Add tests for Set/GetCurrentProcessExplicitAppUserModelID. https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
From: Robert Gerigk <Robert-Gerigk@online.de> Wine's shcore.dll had Set/GetCurrentProcessExplicitAppUserModelID as stubs. SetCurrentProcessExplicitAppUserModelID returned S_OK but discarded the value, and GetCurrentProcessExplicitAppUserModelID always returned NULL with E_NOTIMPL. This broke applications that set an AppUserModelID and later retrieve it, causing "String argument cannot be null or empty" crashes in .NET WPF applications. Implement both functions using a process-wide static variable protected by a critical section. SetCurrentProcessExplicitAppUserModelID stores a CoTaskMemAlloc'd copy of the ID, and GetCurrentProcessExplicitAppUserModelID returns a CoTaskMemAlloc'd copy to the caller. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- dlls/shcore/main.c | 50 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/dlls/shcore/main.c b/dlls/shcore/main.c index f04853cb9f5..5c29a69d4c9 100644 --- a/dlls/shcore/main.c +++ b/dlls/shcore/main.c @@ -249,17 +249,59 @@ HRESULT WINAPI IUnknown_SetSite(IUnknown *obj, IUnknown *site) return hr; } +static WCHAR *explicit_app_user_model_id; +static CRITICAL_SECTION appid_cs; +static CRITICAL_SECTION_DEBUG appid_cs_debug = +{ + 0, 0, &appid_cs, + { &appid_cs_debug.ProcessLocksList, &appid_cs_debug.ProcessLocksList }, + 0, 0, { (DWORD_PTR)(__FILE__ ": appid_cs") } +}; +static CRITICAL_SECTION appid_cs = { &appid_cs_debug, -1, 0, 0, 0, 0 }; + HRESULT WINAPI SetCurrentProcessExplicitAppUserModelID(const WCHAR *appid) { - FIXME("%s: stub\n", debugstr_w(appid)); + WCHAR *new_id = NULL; + + TRACE("%s\n", debugstr_w(appid)); + + if (appid) + { + DWORD len = (lstrlenW(appid) + 1) * sizeof(WCHAR); + new_id = CoTaskMemAlloc(len); + if (!new_id) return E_OUTOFMEMORY; + memcpy(new_id, appid, len); + } + + EnterCriticalSection(&appid_cs); + CoTaskMemFree(explicit_app_user_model_id); + explicit_app_user_model_id = new_id; + LeaveCriticalSection(&appid_cs); + return S_OK; } HRESULT WINAPI GetCurrentProcessExplicitAppUserModelID(const WCHAR **appid) { - FIXME("%p: stub\n", appid); - *appid = NULL; - return E_NOTIMPL; + TRACE("%p\n", appid); + + if (!appid) return E_INVALIDARG; + + EnterCriticalSection(&appid_cs); + if (explicit_app_user_model_id) + { + DWORD len = (lstrlenW(explicit_app_user_model_id) + 1) * sizeof(WCHAR); + *appid = CoTaskMemAlloc(len); + if (*appid) + memcpy((WCHAR *)*appid, explicit_app_user_model_id, len); + } + else + { + *appid = NULL; + } + LeaveCriticalSection(&appid_cs); + + return *appid ? S_OK : E_FAIL; } /************************************************************************* -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
From: Robert Gerigk <Robert-Gerigk@online.de> Test parameter validation, set/get round-trip, and ID update behavior. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- dlls/shcore/tests/Makefile.in | 2 +- dlls/shcore/tests/shcore.c | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/dlls/shcore/tests/Makefile.in b/dlls/shcore/tests/Makefile.in index 2b7eaa65686..d40498fda15 100644 --- a/dlls/shcore/tests/Makefile.in +++ b/dlls/shcore/tests/Makefile.in @@ -1,5 +1,5 @@ TESTDLL = shcore.dll -IMPORTS = advapi32 +IMPORTS = advapi32 ole32 SOURCES = \ shcore.c diff --git a/dlls/shcore/tests/shcore.c b/dlls/shcore/tests/shcore.c index 539c2e5c1bc..88c8bf1669d 100644 --- a/dlls/shcore/tests/shcore.c +++ b/dlls/shcore/tests/shcore.c @@ -23,10 +23,13 @@ #include <windows.h> #include "initguid.h" #include "objidl.h" +#include "objbase.h" #include "shlwapi.h" #include "wine/test.h" +static HRESULT (WINAPI *pSetCurrentProcessExplicitAppUserModelID)(const WCHAR *); +static HRESULT (WINAPI *pGetCurrentProcessExplicitAppUserModelID)(const WCHAR **); static HRESULT (WINAPI *pGetProcessReference)(IUnknown **); static void (WINAPI *pSetProcessReference)(IUnknown *); static HRESULT (WINAPI *pSHGetInstanceExplorer)(IUnknown **); @@ -62,6 +65,8 @@ static const char * initial_buffer ="0123456789"; static void init(HMODULE hshcore) { #define X(f) p##f = (void*)GetProcAddress(hshcore, #f) + X(SetCurrentProcessExplicitAppUserModelID); + X(GetCurrentProcessExplicitAppUserModelID); X(GetProcessReference); X(SetProcessReference); X(SHUnicodeToAnsi); @@ -777,6 +782,46 @@ static void test_stream_size(void) DeleteFileA(filename); } +static void test_AppUserModelID(void) +{ + const WCHAR *appid = NULL; + HRESULT hr; + + if (!pSetCurrentProcessExplicitAppUserModelID || !pGetCurrentProcessExplicitAppUserModelID) + { + win_skip("AppUserModelID functions not available.\n"); + return; + } + + /* Set a valid ID */ + hr = pSetCurrentProcessExplicitAppUserModelID(L"Wine.Test.AppId"); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + /* Get should return the ID we set */ + appid = NULL; + hr = pGetCurrentProcessExplicitAppUserModelID(&appid); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + ok(appid != NULL, "Expected non-NULL appid.\n"); + if (appid) + { + ok(!lstrcmpW(appid, L"Wine.Test.AppId"), "Got %s.\n", wine_dbgstr_w(appid)); + CoTaskMemFree((void *)appid); + } + + /* Set a different ID */ + hr = pSetCurrentProcessExplicitAppUserModelID(L"Wine.Test.AppId2"); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + appid = NULL; + hr = pGetCurrentProcessExplicitAppUserModelID(&appid); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + if (appid) + { + ok(!lstrcmpW(appid, L"Wine.Test.AppId2"), "Got %s.\n", wine_dbgstr_w(appid)); + CoTaskMemFree((void *)appid); + } +} + START_TEST(shcore) { HMODULE hshcore = LoadLibraryA("shcore.dll"); @@ -789,6 +834,7 @@ START_TEST(shcore) init(hshcore); + test_AppUserModelID(); test_process_reference(); test_SHUnicodeToAnsi(); test_SHAnsiToUnicode(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
Would be worth testing what happens if the string length exceeds APPLICATION_USER_MODEL_ID_MAX_LENGTH or is lower than APPLICATION_USER_MODEL_ID_MIN_LENGTH. My old notes say that native returns E_INVALIDARG for strings that are too long but does not seem to enforce the minimum length. MSDN also says "This method must be called during an application's initial startup routine before the application presents any UI or makes any manipulation of its Jump Lists." I didn't see any trace of that behavior on native, and it's probably not something that matters for us to enforce. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135165
On Mon Apr 6 20:20:25 2026 +0000, Tim Clem wrote:
Would be worth testing what happens if the string length exceeds APPLICATION_USER_MODEL_ID_MAX_LENGTH or is lower than APPLICATION_USER_MODEL_ID_MIN_LENGTH. My old notes say that native returns E_INVALIDARG for strings that are too long but does not seem to enforce the minimum length. MSDN also says "This method must be called during an application's initial startup routine before the application presents any UI or makes any manipulation of its Jump Lists." I didn't see any trace of that behavior on native, and it's probably not something that matters for us to enforce. Thanks for the feedback.
For the validation: * Reject NULL and empty strings with E_INVALIDARG * Reject strings exceeding 128 characters (APPLICATION_USER_MODEL_ID_MAX_LENGTH) with E_INVALIDARG * Not enforce a minimum length beyond rejecting empty strings, since you noted native does not seem to enforce it. Does that match your observations on native? I will update the implementation and add tests then. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135167
On Mon Apr 6 20:20:25 2026 +0000, Jan Robert Gerigk wrote:
Thanks for the feedback. For the validation: * Reject NULL and empty strings with E_INVALIDARG * Reject strings exceeding 128 characters (APPLICATION_USER_MODEL_ID_MAX_LENGTH) with E_INVALIDARG * Not enforce a minimum length beyond rejecting empty strings, since you noted native does not seem to enforce it. Does that match your observations on native? I will update the implementation and add tests then. That's what I have down in my notes, yes. Sounds good!
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135168
participants (3)
-
Jan Robert Gerigk (@RgSg86) -
Robert Gerigk -
Tim Clem (@tclem)