[PATCH v10 0/2] MR10460: msvcrt: Add support for %Z printf specifier.
This specifier allows printing the `UNICODE_STRING` and `ANSI_STRING` structures. It is useful for running unit tests for kernel mode or shared components on WINE. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 -- v10: msvcrt/tests: Add tests for %Z printf specifier. msvcrt: Add support for %Z printf specifier. https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
From: Trung Nguyen <me@trungnt2910.com> This specifier allows printing the `UNICODE_STRING` and `ANSI_STRING` structures. It is useful for running unit tests for kernel mode or shared components on WINE. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 --- dlls/msvcrt/printf.h | 47 +++++++++++++++++++++++++++++++++++++------- dlls/msvcrt/wcs.c | 1 + 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/dlls/msvcrt/printf.h b/dlls/msvcrt/printf.h index cfcfd5be771..9cc9c3b237c 100644 --- a/dlls/msvcrt/printf.h +++ b/dlls/msvcrt/printf.h @@ -255,11 +255,34 @@ static inline int FUNC_NAME(pf_output_format_str)(FUNC_NAME(puts_clbk) pf_puts, return r>=0 ? ret : r; } -static inline int FUNC_NAME(pf_handle_string)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, - const void *str, int len, pf_flags *flags, _locale_t locale, BOOL legacy_wide) +static inline BOOL FUNC_NAME(pf_is_str_wide)(pf_flags *flags, BOOL legacy_wide) { BOOL api_is_wide = sizeof(APICHAR) == sizeof(wchar_t); BOOL complement_is_narrow = legacy_wide ? api_is_wide : FALSE; + + if((flags->NaturalString && api_is_wide) || flags->WideString || flags->IntegerLength == LEN_LONG) + return TRUE; + if((flags->NaturalString && !api_is_wide) || flags->IntegerLength == LEN_SHORT) + return FALSE; + +#if _MSVCR_VER >= 140 + if (flags->Format == 'Z') + return legacy_wide ? !api_is_wide : TRUE; +#else + if (flags->Format == 'Z') + return FALSE; +#endif + + if((flags->Format=='S' || flags->Format=='C') == complement_is_narrow) + return FALSE; + else + return TRUE; +} + +static inline int FUNC_NAME(pf_handle_string)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, + const void *str, int len, pf_flags *flags, _locale_t locale, BOOL legacy_wide) +{ + BOOL str_is_wide; #ifdef PRINTF_WIDE if(!str) @@ -269,12 +292,13 @@ static inline int FUNC_NAME(pf_handle_string)(FUNC_NAME(puts_clbk) pf_puts, void return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, "(null)", 6, flags, locale); #endif - if((flags->NaturalString && api_is_wide) || flags->WideString || flags->IntegerLength == LEN_LONG) - return FUNC_NAME(pf_output_format_wstr)(pf_puts, puts_ctx, str, len, flags, locale); - if((flags->NaturalString && !api_is_wide) || flags->IntegerLength == LEN_SHORT) - return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, str, len, flags, locale); + str_is_wide = FUNC_NAME(pf_is_str_wide)(flags, legacy_wide); - if((flags->Format=='S' || flags->Format=='C') == complement_is_narrow) + /* UNICODE_STRING passes the number of bytes. */ + if(flags->Format=='Z' && str_is_wide && len != -1) + len /= sizeof(wchar_t); + + if(!str_is_wide) return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, str, len, flags, locale); else return FUNC_NAME(pf_output_format_wstr)(pf_puts, puts_ctx, str, len, flags, locale); @@ -1142,6 +1166,15 @@ int FUNC_NAME(pf_printf)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, const API i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, &ch, 1, &flags, locale, legacy_wide); if(i < 0) i = 0; /* ignore conversion error */ + } else if(flags.Format == 'Z') { + /* UNICODE_STRING and ANSI_STRING have the same layout. */ + UNICODE_STRING *str = pf_args(args_ctx, pos, VT_PTR, valist).get_ptr; + if(!str) + i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, NULL, -1, + &flags, locale, legacy_wide); + else + i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, str->Buffer, str->Length, + &flags, locale, legacy_wide); } else if(flags.Format == 'p') { flags.Format = 'X'; flags.PadZero = TRUE; diff --git a/dlls/msvcrt/wcs.c b/dlls/msvcrt/wcs.c index 1b7bc3e54e0..9ad7181311a 100644 --- a/dlls/msvcrt/wcs.c +++ b/dlls/msvcrt/wcs.c @@ -28,6 +28,7 @@ #include <wctype.h> #include "msvcrt.h" #include "winnls.h" +#include "winternl.h" #include "wtypes.h" #include "wine/debug.h" -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
From: Trung Nguyen <me@trungnt2910.com> Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 --- dlls/msvcrt/tests/printf.c | 13 +++++++++++++ dlls/ucrtbase/tests/printf.c | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/dlls/msvcrt/tests/printf.c b/dlls/msvcrt/tests/printf.c index c21b1285275..30e5d15db87 100644 --- a/dlls/msvcrt/tests/printf.c +++ b/dlls/msvcrt/tests/printf.c @@ -33,6 +33,7 @@ #include "windef.h" #include "winbase.h" #include "winnls.h" +#include "winternl.h" #include "wine/test.h" @@ -170,6 +171,7 @@ static void test_sprintf( void ) { "% d", " 1", 0, INT_ARG, 1 }, { "%+ d", "+1", 0, INT_ARG, 1 }, { "%S", "wide", 0, PTR_ARG, 0, 0, 0, L"wide" }, + { "%Z", "ansi", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 4, 4, (char *)"ansi" } }, { "%04c", "0001", 0, INT_ARG, '1' }, { "%-04c", "1 ", 0, INT_ARG, '1' }, { "%#012x", "0x0000000001", 0, INT_ARG, 1 }, @@ -194,6 +196,8 @@ static void test_sprintf( void ) { "%w-s", "-s", 0, PTR_ARG, 0, 0, 0, L"wide" }, { "%ls", "wide", 0, PTR_ARG, 0, 0, 0, L"wide" }, { "%Ls", "not wide", 0, PTR_ARG, 0, 0, 0, "not wide" }, + { "%wZ", "wide", 0, PTR_ARG, 0, 0, 0, &(UNICODE_STRING){ 8, 8, (wchar_t *)L"wide" } }, + { "%hZ", "not wide", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 8, 8, (char *)"not wide" } }, { "%b", "b", 0, NO_ARG }, { "%3c", " a", 0, INT_ARG, 'a' }, { "%3d", "1234", 0, INT_ARG, 1234 }, @@ -215,6 +219,10 @@ static void test_sprintf( void ) { "%o", "12", 0, INT_ARG, 10 }, { "%s", "(null)", 0, PTR_ARG, 0, 0, 0, NULL }, { "%s", "%%%%", 0, PTR_ARG, 0, 0, 0, "%%%%" }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, NULL }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 0, 0, NULL } }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 1, 1, NULL } }, + { "%Z", "wi", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 2, 4, (char *)"wide" } }, { "%u", "4294967295", 0, INT_ARG, -1 }, { "%w", "", 0, INT_ARG, -1 }, { "%h", "", 0, INT_ARG, -1 }, @@ -441,6 +449,7 @@ static void test_swprintf( void ) wchar_t buffer[100]; double pnumber = 789456123; const char string[] = "string"; + ANSI_STRING ansi_string = { 11, 11, (char *)"ansi string" }; swprintf(buffer, L"%+#23.15e", pnumber); ok(wcsstr(buffer, L"e+008") != 0, "Sprintf different\n"); @@ -450,6 +459,10 @@ static void test_swprintf( void ) ok(wcslen(buffer) == 6, "Problem with \"%%S\" interpretation\n"); swprintf(buffer, L"%hs", string); ok(!wcscmp(L"string", buffer), "swprintf failed with %%hs\n"); + swprintf(buffer, L"%Z", &ansi_string); + ok(!wcscmp(L"ansi string", buffer), "swprintf failed with %%Z\n"); + swprintf(buffer, L"%hZ", &ansi_string); + ok(!wcscmp(L"ansi string", buffer), "swprintf failed with %%hZ\n"); } static void test_snprintf (void) diff --git a/dlls/ucrtbase/tests/printf.c b/dlls/ucrtbase/tests/printf.c index 16dd4f3727b..7f90202a768 100644 --- a/dlls/ucrtbase/tests/printf.c +++ b/dlls/ucrtbase/tests/printf.c @@ -29,6 +29,7 @@ #include "windef.h" #include "winbase.h" #include "winnls.h" +#include "winternl.h" #include "wine/test.h" @@ -533,14 +534,19 @@ static void test_printf_legacy_wide(void) { const wchar_t wide[] = {'A','B','C','D',0}; const char narrow[] = "abcd"; + const UNICODE_STRING wide_str = { 8, sizeof(wide), (wchar_t *)wide }; + const ANSI_STRING narrow_str = { 4, sizeof(narrow), (char *)narrow }; const char out[] = "abcd ABCD"; /* The legacy wide flag doesn't affect narrow printfs, so the same * format should behave the same both with and without the flag. */ const char narrow_fmt[] = "%s %ls"; + const char narrow_str_fmt[] = "%hZ %Z"; /* The standard behaviour is to use the same format as for the narrow * case, while the legacy case has got a different meaning for %s. */ const wchar_t std_wide_fmt[] = {'%','s',' ','%','l','s',0}; const wchar_t legacy_wide_fmt[] = {'%','h','s',' ','%','s',0}; + const wchar_t std_wide_str_fmt[] = {'%','h','Z',' ','%','Z',0}; + const wchar_t legacy_wide_str_fmt[] = {'%','Z',' ','%','l','Z',0}; char buffer[20]; wchar_t wbuffer[20]; @@ -549,12 +555,24 @@ static void test_printf_legacy_wide(void) vsprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, buffer, sizeof(buffer), narrow_fmt, narrow, wide); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vsprintf_wrapper(0, buffer, sizeof(buffer), narrow_str_fmt, &narrow_str, &wide_str); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vsprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, buffer, sizeof(buffer), narrow_str_fmt, &narrow_str, &wide_str); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vswprintf_wrapper(0, wbuffer, sizeof(wbuffer), std_wide_fmt, narrow, wide); WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); vswprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, wbuffer, sizeof(wbuffer), legacy_wide_fmt, narrow, wide); WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + + vswprintf_wrapper(0, wbuffer, sizeof(wbuffer), std_wide_str_fmt, &narrow_str, &wide_str); + WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vswprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, wbuffer, sizeof(wbuffer), legacy_wide_str_fmt, &narrow_str, &wide_str); + WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); } static void test_printf_legacy_msvcrt(void) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
Please also add %Z format test in swprintf test (to show that it uses ANSI_STRING).
I added a case that uses `%Z` in `test_swprintf`.
Please also check the function behavior with ucrtbase - current implementation will fail there. Its behavior probably changes based on \_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS or \_CRT_INTERNAL_PRINTF_LEGACY_MSVCRT_COMPATIBILITY flag.
I did some experiments on an `ucrt`-based toolchain: https://godbolt.org/z/sYxzWbM3x Without any legacy flag, `%Z` is always treated as wide. With `_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS`, `%Z` is treated as narrow by wide functions and wide by narrow functions. `_CRT_INTERNAL_PRINTF_LEGACY_MSVCRT_COMPATIBILITY` has no effect. My previous research was done on clang/MinGW, and they probably did something weird there.
Please also add tests to ucrtbase.
I added tests to `test_printf_legacy_wide` to demonstrate the behavior. I checked and confirmed that it worked on Windows 11 25H2.
This is not portable, please define ANSI_STRING/UNICODE_STRING structures separately and use pointers to them here.
As I mentioned in the comment above, are you referring to the object literals or the structure definitions themselves? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134651
On Wed Apr 1 11:22:30 2026 +0000, Trung Nguyen wrote:
changed this line in [version 10 of the diff](/wine/wine/-/merge_requests/10460/diffs?diff_id=256631&start_sha=9ff888a170bada2c5e1b7b6385c0fd7a421484a1#07f58557da1f6a172fb851181f5958a07643494b_205_203) I moved the handling to `pf_handle_string`. Doing it further down the call stack, such as directly in `pf_printf`, will cause massive code duplication, since `pf_printf` does not know which character type it got passed.
To prevent code duplication I also moved the wide/narrow detection logic in `pf_handle_string` to a separate `static` `inline` function, `pf_is_str_wide`. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134652
On Wed Apr 1 11:30:45 2026 +0000, Trung Nguyen wrote:
Please also add %Z format test in swprintf test (to show that it uses ANSI_STRING). I added a case that uses `%Z` in `test_swprintf`. Please also check the function behavior with ucrtbase - current implementation will fail there. Its behavior probably changes based on \_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS or \_CRT_INTERNAL_PRINTF_LEGACY_MSVCRT_COMPATIBILITY flag. I did some experiments on an `ucrt`-based toolchain: https://godbolt.org/z/sYxzWbM3x Without any legacy flag, `%Z` is always treated as wide. With `_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS`, `%Z` is treated as narrow by wide functions and wide by narrow functions. `_CRT_INTERNAL_PRINTF_LEGACY_MSVCRT_COMPATIBILITY` has no effect. My previous research was done on clang/MinGW, and they probably did something weird there. Please also add tests to ucrtbase. I added tests to `test_printf_legacy_wide` to demonstrate the behavior. I checked and confirmed that it worked on Windows 11 25H2. This is not portable, please define ANSI_STRING/UNICODE_STRING structures separately and use pointers to them here. As I mentioned in the comment above, are you referring to the object literals or the structure definitions themselves? Please ignore the comment about portability - I was wrong. Sorry about that.
The results for ucrtbase are strange. The documentation states that ucrtbase has a bug that is preserved for backward compatibility. Unfortunately this means that ucrtbase needs to be special-cased. I was hoping that it can be avoided by using proper flags. I have added some minor changes to your patches: [aefe9521b1e20674f059fa52be45d45a47c44d88](https://gitlab.winehq.org/piotr/wine/-/commit/aefe9521b1e20674f059fa52be45d4...), [c84e147eceeb2a48e1053bf9e4ab7cb6ea243f9e](https://gitlab.winehq.org/piotr/wine/-/commit/c84e147eceeb2a48e1053bf9e4ab7c...). If it looks good please push the patches to this MR. Here's a short list of changes: * added more tests * fixed handling of UNICODE_STRING with broken length * pass length in characters to pf_handle_string function * simplify pf_is_str_wide helper -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134682
participants (3)
-
Piotr Caban (@piotr) -
Trung Nguyen -
Trung Nguyen (@trungnt2910)