[libc-commits] [libc] [libc] Support %lc in printf (PR #169983)

Shubh Pachchigar via libc-commits libc-commits at lists.llvm.org
Sat Dec 20 00:44:37 PST 2025


https://github.com/shubhe25p updated https://github.com/llvm/llvm-project/pull/169983

>From 552b2635399b1f614ce9e069fa8b0c21e0a89247 Mon Sep 17 00:00:00 2001
From: "shubh at DOE" <shubhp at mbm3a24.local>
Date: Sun, 7 Dec 2025 14:55:03 -0800
Subject: [PATCH 1/5] [libc] Support %lc in printf

Add support for %lc in printf by calling internal
wcrtomb function and relevant end-to-end snprintf
test. Additionally, printf parser can also
recognize length modifier. Added a flag to disable
wchar support on windows platform.
---
 libc/docs/dev/printf_behavior.rst             |  7 +++
 libc/src/stdio/printf_core/CMakeLists.txt     | 16 ++++++
 libc/src/stdio/printf_core/char_converter.h   | 34 +++++++++++-
 libc/src/stdio/printf_core/parser.h           | 13 +++--
 libc/test/src/stdio/CMakeLists.txt            |  9 ++++
 .../test/src/stdio/printf_core/CMakeLists.txt | 12 +++++
 .../src/stdio/printf_core/parser_test.cpp     | 19 +++++++
 libc/test/src/stdio/snprintf_test.cpp         | 54 +++++++++++++++++++
 8 files changed, 158 insertions(+), 6 deletions(-)

diff --git a/libc/docs/dev/printf_behavior.rst b/libc/docs/dev/printf_behavior.rst
index 01ab128a1f238..ba0578aee3fd8 100644
--- a/libc/docs/dev/printf_behavior.rst
+++ b/libc/docs/dev/printf_behavior.rst
@@ -71,6 +71,13 @@ conversions (%r, %k); any fixed point number conversion will be treated as
 invalid. This reduces code size. This has no effect if the current compiler does
 not support fixed point numbers.
 
+LIBC_COPT_PRINTF_DISABLE_WIDE
+--------------------------------
+When set, this flag disables support for wide characters (%lc and %ls). Any
+conversions will be ignored. This reduces code size. This will be set by default
+on windows platforms as current printf implementation does not support UTF-16 wide
+characters.
+
 LIBC_COPT_PRINTF_NO_NULLPTR_CHECKS
 ----------------------------------
 When set, this flag disables the nullptr checks in %n and %s.
diff --git a/libc/src/stdio/printf_core/CMakeLists.txt b/libc/src/stdio/printf_core/CMakeLists.txt
index 624129b2b36e7..0fe044938e40f 100644
--- a/libc/src/stdio/printf_core/CMakeLists.txt
+++ b/libc/src/stdio/printf_core/CMakeLists.txt
@@ -43,6 +43,14 @@ if(NOT TARGET ${target_error_mapper})
     set(target_error_mapper libc.src.stdio.printf_core.generic.error_mapper)
 endif()
 
+set(wchar_deps libc.src.__support.wchar.wcrtomb)
+set(wchar_compile_flags "")
+
+if(${LIBC_TARGET_OS} STREQUAL "windows" OR WIN32)
+  set(wchar_deps "")
+  list(APPEND wchar_compile_flags "-DLIBC_COPT_PRINTF_DISABLE_WIDE")
+endif()
+
 add_header_library(
   printf_config
   HDRS
@@ -67,6 +75,7 @@ add_header_library(
     parser.h
   DEPENDS
     .core_structs
+    libc.hdr.types.wint_t
     libc.src.__support.arg_list
     libc.src.__support.ctype_utils
     libc.src.__support.str_to_integer
@@ -106,11 +115,16 @@ add_header_library(
     float_dec_converter.h
     fixed_converter.h #TODO: Check if this should be disabled when fixed unavail
     strerror_converter.h
+  COMPILE_OPTIONS
+    ${wchar_compile_flags}
   DEPENDS
     .core_structs
     .printf_config
     .writer
     libc.include.inttypes
+    libc.hdr.types.wchar_t
+    libc.hdr.types.wint_t
+    libc.hdr.wchar_macros
     libc.src.__support.big_int
     libc.src.__support.common
     libc.src.__support.CPP.limits
@@ -123,8 +137,10 @@ add_header_library(
     libc.src.__support.integer_to_string
     libc.src.__support.libc_assert
     libc.src.__support.uint128
+    libc.src.__support.wchar.mbstate
     libc.src.__support.StringUtil.error_to_string
     libc.src.string.memory_utils.inline_memcpy
+    ${wchar_deps}
 )
 
 add_header_library(
diff --git a/libc/src/stdio/printf_core/char_converter.h b/libc/src/stdio/printf_core/char_converter.h
index fd2eb2553887a..ed0546aa860f6 100644
--- a/libc/src/stdio/printf_core/char_converter.h
+++ b/libc/src/stdio/printf_core/char_converter.h
@@ -1,4 +1,4 @@
-//===-- String Converter for printf -----------------------------*- C++ -*-===//
+//===-- Character Converter for printf --------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -9,6 +9,15 @@
 #ifndef LLVM_LIBC_SRC_STDIO_PRINTF_CORE_CHAR_CONVERTER_H
 #define LLVM_LIBC_SRC_STDIO_PRINTF_CORE_CHAR_CONVERTER_H
 
+#include "hdr/types/wchar_t.h"
+#include "hdr/types/wint_t.h"
+#include "hdr/wchar_macros.h"
+#include "src/__support/wchar/mbstate.h"
+
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+#include "src/__support/wchar/wcrtomb.h"
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+
 #include "src/__support/macros/config.h"
 #include "src/stdio/printf_core/converter_utils.h"
 #include "src/stdio/printf_core/core_structs.h"
@@ -33,7 +42,28 @@ LIBC_INLINE int convert_char(Writer<write_mode> *writer,
     RET_IF_RESULT_NEGATIVE(writer->write(' ', padding_spaces));
   }
 
-  RET_IF_RESULT_NEGATIVE(writer->write(c));
+  if (to_conv.length_modifier == LengthModifier::l) {
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+    wint_t wi = static_cast<wint_t>(to_conv.conv_val_raw);
+
+    if (wi == WEOF) {
+      return -1;
+    }
+
+    char mb_str[MB_LEN_MAX];
+    internal::mbstate mbstate;
+    wchar_t wc = static_cast<wchar_t>(wi);
+
+    auto ret = internal::wcrtomb(mb_str, wc, &mbstate);
+    if (!ret.has_value()) {
+      return -1;
+    }
+
+    RET_IF_RESULT_NEGATIVE(writer->write({mb_str, ret.value()}));
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+  } else {
+    RET_IF_RESULT_NEGATIVE(writer->write(c));
+  }
 
   // If the padding is on the right side, write the spaces last.
   if (padding_spaces > 0 &&
diff --git a/libc/src/stdio/printf_core/parser.h b/libc/src/stdio/printf_core/parser.h
index cef9b1ae58fa0..dea1ff966b6bb 100644
--- a/libc/src/stdio/printf_core/parser.h
+++ b/libc/src/stdio/printf_core/parser.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_LIBC_SRC_STDIO_PRINTF_CORE_PARSER_H
 #define LLVM_LIBC_SRC_STDIO_PRINTF_CORE_PARSER_H
 
+#include "hdr/types/wint_t.h"
 #include "include/llvm-libc-macros/stdfix-macros.h"
 #include "src/__support/CPP/algorithm.h" // max
 #include "src/__support/CPP/limits.h"
@@ -73,9 +74,9 @@ template <typename ArgProvider> class Parser {
   ArgProvider args_cur;
 
 #ifndef LIBC_COPT_PRINTF_DISABLE_INDEX_MODE
-  // args_start stores the start of the va_args, which is allows getting the
-  // value of arguments that have already been passed. args_index is tracked so
-  // that we know which argument args_cur is on.
+  // args_start stores the start of the va_args, which helps in getting the
+  // number of arguments that have already been passed. args_index is tracked
+  // so that we know which argument args_cur is on.
   ArgProvider args_start;
   size_t args_index = 1;
 
@@ -173,7 +174,11 @@ template <typename ArgProvider> class Parser {
         section.has_conv = true;
         break;
       case ('c'):
-        WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, int, conv_index);
+        if (section.length_modifier == LengthModifier::l) {
+          WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, wint_t, conv_index);
+        } else {
+          WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, int, conv_index);
+        }
         break;
       case ('d'):
       case ('i'):
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index a39428fb8d16c..0ef56c865ec53 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -137,6 +137,11 @@ if(LIBC_CONF_PRINTF_DISABLE_STRERROR)
   list(APPEND sprintf_test_copts "-DLIBC_COPT_PRINTF_DISABLE_STRERROR")
 endif()
 
+set(snprintf_test_flags "")
+if(${LIBC_TARGET_OS} STREQUAL "windows" OR WIN32)
+  list(APPEND snprintf_test_flags "-DLIBC_COPT_PRINTF_DISABLE_WIDE")
+endif()
+
 add_fp_unittest(
   sprintf_test
   UNIT_TEST_ONLY
@@ -158,7 +163,11 @@ add_libc_test(
     libc_stdio_unittests
   SRCS
     snprintf_test.cpp
+  COMPILE_OPTIONS
+    ${snprintf_test_flags}
   DEPENDS
+    libc.hdr.types.wchar_t
+    libc.hdr.wchar_macros
     libc.src.stdio.snprintf
 )
 
diff --git a/libc/test/src/stdio/printf_core/CMakeLists.txt b/libc/test/src/stdio/printf_core/CMakeLists.txt
index ff7ebbc4f5fd0..1da5ffaa6fef2 100644
--- a/libc/test/src/stdio/printf_core/CMakeLists.txt
+++ b/libc/test/src/stdio/printf_core/CMakeLists.txt
@@ -1,3 +1,8 @@
+set(printf_test_flags "")
+if(${LIBC_TARGET_OS} STREQUAL "windows" OR WIN32)
+  list(APPEND printf_test_flags "-DLIBC_COPT_PRINTF_DISABLE_WIDE")
+endif()
+
 add_libc_unittest(
   parser_test
   SUITE
@@ -6,7 +11,10 @@ add_libc_unittest(
     parser_test.cpp
   LINK_LIBRARIES
     LibcPrintfHelpers
+  COMPILE_OPTIONS
+    ${printf_test_flags}
   DEPENDS
+    libc.hdr.types.wchar_t
     libc.src.stdio.printf_core.parser
     libc.src.stdio.printf_core.core_structs
     libc.src.__support.CPP.string_view
@@ -19,6 +27,8 @@ add_libc_unittest(
     libc_stdio_unittests
   SRCS
     writer_test.cpp
+  COMPILE_OPTIONS
+    ${printf_test_flags}
   DEPENDS
     libc.src.stdio.printf_core.writer
     libc.src.string.memory_utils.inline_memcpy
@@ -31,6 +41,8 @@ add_libc_unittest(
     libc_stdio_unittests
   SRCS
     converter_test.cpp
+  COMPILE_OPTIONS
+    ${printf_test_flags}
   DEPENDS
     libc.src.stdio.printf_core.converter
     libc.src.stdio.printf_core.writer
diff --git a/libc/test/src/stdio/printf_core/parser_test.cpp b/libc/test/src/stdio/printf_core/parser_test.cpp
index 9d192828860f7..b2edf2b61d992 100644
--- a/libc/test/src/stdio/printf_core/parser_test.cpp
+++ b/libc/test/src/stdio/printf_core/parser_test.cpp
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "hdr/types/wchar_t.h"
 #include "src/__support/CPP/bit.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/arg_list.h"
@@ -370,6 +371,24 @@ TEST(LlvmLibcPrintfParserTest,
   ASSERT_PFORMAT_EQ(expected, format_arr[0]);
 }
 
+TEST(LlvmLibcPrintfParserTest, EvalOneArgWithWideCharacter) {
+  LIBC_NAMESPACE::printf_core::FormatSection format_arr[2];
+  const char *str = "%lc";
+  wchar_t arg1 = L'€';
+  evaluate(format_arr, str, arg1);
+
+  LIBC_NAMESPACE::printf_core::FormatSection expected;
+  expected.has_conv = true;
+
+  expected.raw_string = {str, 3};
+  expected.length_modifier = LIBC_NAMESPACE::printf_core::LengthModifier::l;
+  expected.conv_val_raw =
+      static_cast<LIBC_NAMESPACE::fputil::FPBits<double>::StorageType>(arg1);
+  expected.conv_name = 'c';
+
+  ASSERT_PFORMAT_EQ(expected, format_arr[0]);
+}
+
 #ifndef LIBC_COPT_PRINTF_DISABLE_INDEX_MODE
 
 TEST(LlvmLibcPrintfParserTest, IndexModeOneArg) {
diff --git a/libc/test/src/stdio/snprintf_test.cpp b/libc/test/src/stdio/snprintf_test.cpp
index 95507e0885dbf..a827085e63085 100644
--- a/libc/test/src/stdio/snprintf_test.cpp
+++ b/libc/test/src/stdio/snprintf_test.cpp
@@ -6,6 +6,8 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "hdr/types/wchar_t.h"
+#include "hdr/wchar_macros.h"
 #include "src/stdio/snprintf.h"
 
 #include "test/UnitTest/ErrnoCheckingTest.h"
@@ -74,3 +76,55 @@ TEST(LlvmLibcSNPrintfTest, CharsWrittenOverflow) {
   EXPECT_LT(written, 0);
   ASSERT_ERRNO_FAILURE();
 }
+
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+TEST(LlvmLibcSNPrintfTest, WideCharConversion) {
+  char buff[16];
+  int written;
+
+  written = LIBC_NAMESPACE::snprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 3);
+  ASSERT_STREQ(buff, "€");
+}
+
+TEST(LlvmLibcSNPrintfTest, WideCharConversionLeftJustified) {
+  char buff[16];
+  int written;
+
+  written = LIBC_NAMESPACE::snprintf(buff, sizeof(buff), "%-4lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 6);
+  ASSERT_STREQ(buff, "€   ");
+}
+
+TEST(LlvmLibcSNPrintfTest, WideCharConversionRightJustified) {
+  char buff[16];
+  int written;
+
+  written = LIBC_NAMESPACE::snprintf(buff, sizeof(buff), "%4lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 6);
+  ASSERT_STREQ(buff, "   €");
+}
+
+TEST(LlvmLibcSNPrintfTest, WideCharWEOFConversion) {
+  char buff[16];
+  int written;
+
+  written = LIBC_NAMESPACE::snprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(WEOF));
+  EXPECT_EQ(written, -1);
+  ASSERT_ERRNO_FAILURE();
+}
+
+TEST(LlvmLibcSNPrintfTest, WideCharInvalidConversion) {
+  char buff[16];
+  int written;
+
+  written = LIBC_NAMESPACE::snprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(0x12ffff));
+  EXPECT_EQ(written, -1);
+  ASSERT_ERRNO_FAILURE();
+}
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE

>From ab1034d162ffc52b32dbb9ac3debcbb4d9248e5d Mon Sep 17 00:00:00 2001
From: "shubh at DOE" <shubhp at mbm3a24.local>
Date: Fri, 19 Dec 2025 16:16:42 -0800
Subject: [PATCH 2/5] test

---
 libc/config/windows/config.json             |  8 ++++
 libc/docs/dev/printf_behavior.rst           |  7 ++++
 libc/src/stdio/printf_core/CMakeLists.txt   | 14 +++++++
 libc/src/stdio/printf_core/char_converter.h | 32 ++++++++++++++-
 libc/src/stdio/printf_core/parser.h         | 22 +++++++++--
 libc/test/src/stdio/CMakeLists.txt          | 11 ++++++
 libc/test/src/stdio/sprintf_test.cpp        | 43 +++++++++++++++++++++
 7 files changed, 131 insertions(+), 6 deletions(-)
 create mode 100644 libc/config/windows/config.json

diff --git a/libc/config/windows/config.json b/libc/config/windows/config.json
new file mode 100644
index 0000000000000..6fab0387dbb5f
--- /dev/null
+++ b/libc/config/windows/config.json
@@ -0,0 +1,8 @@
+{
+    "printf": {
+      "LIBC_COPT_PRINTF_DISBALE_WIDE": {
+        "value": "true",
+        "doc": "Disable handling wide characters for printf and friends."
+      }
+    }
+}
\ No newline at end of file
diff --git a/libc/docs/dev/printf_behavior.rst b/libc/docs/dev/printf_behavior.rst
index 01ab128a1f238..ba0578aee3fd8 100644
--- a/libc/docs/dev/printf_behavior.rst
+++ b/libc/docs/dev/printf_behavior.rst
@@ -71,6 +71,13 @@ conversions (%r, %k); any fixed point number conversion will be treated as
 invalid. This reduces code size. This has no effect if the current compiler does
 not support fixed point numbers.
 
+LIBC_COPT_PRINTF_DISABLE_WIDE
+--------------------------------
+When set, this flag disables support for wide characters (%lc and %ls). Any
+conversions will be ignored. This reduces code size. This will be set by default
+on windows platforms as current printf implementation does not support UTF-16 wide
+characters.
+
 LIBC_COPT_PRINTF_NO_NULLPTR_CHECKS
 ----------------------------------
 When set, this flag disables the nullptr checks in %n and %s.
diff --git a/libc/src/stdio/printf_core/CMakeLists.txt b/libc/src/stdio/printf_core/CMakeLists.txt
index 624129b2b36e7..7d4a6dc79ea8c 100644
--- a/libc/src/stdio/printf_core/CMakeLists.txt
+++ b/libc/src/stdio/printf_core/CMakeLists.txt
@@ -43,6 +43,18 @@ if(NOT TARGET ${target_error_mapper})
     set(target_error_mapper libc.src.stdio.printf_core.generic.error_mapper)
 endif()
 
+if(LIBC_COPT_PRINTF_DISABLE_WIDE)
+  set(wchar_deps "")
+else()
+  set(wchar_deps
+    libc.hdr.types.wchar_t
+    libc.hdr.types.wint_t
+    libc.hdr.wchar_macros
+    libc.src.__support.wchar.wcrtomb
+    libc.src.__support.wchar.mbstate
+  )
+endif()
+
 add_header_library(
   printf_config
   HDRS
@@ -76,6 +88,7 @@ add_header_library(
     libc.src.__support.CPP.string_view
     libc.src.__support.CPP.type_traits
     libc.src.__support.common
+    ${wchar_deps}
 )
 
 add_header_library(
@@ -125,6 +138,7 @@ add_header_library(
     libc.src.__support.uint128
     libc.src.__support.StringUtil.error_to_string
     libc.src.string.memory_utils.inline_memcpy
+    ${wchar_deps}
 )
 
 add_header_library(
diff --git a/libc/src/stdio/printf_core/char_converter.h b/libc/src/stdio/printf_core/char_converter.h
index fd2eb2553887a..f8c8164cebba7 100644
--- a/libc/src/stdio/printf_core/char_converter.h
+++ b/libc/src/stdio/printf_core/char_converter.h
@@ -1,4 +1,4 @@
-//===-- String Converter for printf -----------------------------*- C++ -*-===//
+//===-- Character Converter for printf --------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -9,6 +9,14 @@
 #ifndef LLVM_LIBC_SRC_STDIO_PRINTF_CORE_CHAR_CONVERTER_H
 #define LLVM_LIBC_SRC_STDIO_PRINTF_CORE_CHAR_CONVERTER_H
 
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+#include "hdr/types/wchar_t.h"
+#include "hdr/types/wint_t.h"
+#include "hdr/wchar_macros.h"
+#include "src/__support/wchar/wcrtomb.h"
+#include "src/__support/wchar/mbstate.h"
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+
 #include "src/__support/macros/config.h"
 #include "src/stdio/printf_core/converter_utils.h"
 #include "src/stdio/printf_core/core_structs.h"
@@ -33,7 +41,27 @@ LIBC_INLINE int convert_char(Writer<write_mode> *writer,
     RET_IF_RESULT_NEGATIVE(writer->write(' ', padding_spaces));
   }
 
-  RET_IF_RESULT_NEGATIVE(writer->write(c));
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE  
+  if (to_conv.length_modifier == LengthModifier::l) {
+    wint_t wi = static_cast<wint_t>(to_conv.conv_val_raw);
+
+    if (wi == WEOF) {
+      return -1;
+    }
+
+    char mb_str[MB_LEN_MAX];
+    internal::mbstate mbstate;
+    wchar_t wc = static_cast<wchar_t>(wi);
+
+    auto ret = internal::wcrtomb(mb_str, wc, &mbstate);
+    if (!ret.has_value()) {
+      return -1;
+    }
+
+    RET_IF_RESULT_NEGATIVE(writer->write({mb_str, ret.value()}));
+  } else
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+    RET_IF_RESULT_NEGATIVE(writer->write(c));
 
   // If the padding is on the right side, write the spaces last.
   if (padding_spaces > 0 &&
diff --git a/libc/src/stdio/printf_core/parser.h b/libc/src/stdio/printf_core/parser.h
index cef9b1ae58fa0..f97d93081a352 100644
--- a/libc/src/stdio/printf_core/parser.h
+++ b/libc/src/stdio/printf_core/parser.h
@@ -9,6 +9,10 @@
 #ifndef LLVM_LIBC_SRC_STDIO_PRINTF_CORE_PARSER_H
 #define LLVM_LIBC_SRC_STDIO_PRINTF_CORE_PARSER_H
 
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+#include "hdr/types/wint_t.h"
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+
 #include "include/llvm-libc-macros/stdfix-macros.h"
 #include "src/__support/CPP/algorithm.h" // max
 #include "src/__support/CPP/limits.h"
@@ -73,9 +77,9 @@ template <typename ArgProvider> class Parser {
   ArgProvider args_cur;
 
 #ifndef LIBC_COPT_PRINTF_DISABLE_INDEX_MODE
-  // args_start stores the start of the va_args, which is allows getting the
-  // value of arguments that have already been passed. args_index is tracked so
-  // that we know which argument args_cur is on.
+  // args_start stores the start of the va_args, which helps in getting the
+  // number of arguments that have already been passed. args_index is tracked
+  // so that we know which argument args_cur is on.
   ArgProvider args_start;
   size_t args_index = 1;
 
@@ -173,7 +177,12 @@ template <typename ArgProvider> class Parser {
         section.has_conv = true;
         break;
       case ('c'):
-        WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, int, conv_index);
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+        if (section.length_modifier == LengthModifier::l) {
+          WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, wint_t, conv_index);
+        } else
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+          WRITE_ARG_VAL_SIMPLEST(section.conv_val_raw, int, conv_index);
         break;
       case ('d'):
       case ('i'):
@@ -574,6 +583,11 @@ template <typename ArgProvider> class Parser {
           conv_size = type_desc_from_type<void>();
           break;
         case ('c'):
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+          if (lm == LengthModifier::l) {
+            conv_size = type_desc_from_type<wint_t>();
+          } else
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
           conv_size = type_desc_from_type<int>();
           break;
         case ('d'):
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index a39428fb8d16c..aa8a02649deac 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -137,6 +137,16 @@ if(LIBC_CONF_PRINTF_DISABLE_STRERROR)
   list(APPEND sprintf_test_copts "-DLIBC_COPT_PRINTF_DISABLE_STRERROR")
 endif()
 
+if(LIBC_COPT_PRINTF_DISABLE_WIDE)
+  list(APPEND sprintf_test_copts "-DLIBC_COPT_PRINTF_DISABLE_WIDE")
+  set(wchar_deps "")
+else()
+  set(wchar_deps
+    libc.hdr.types.wchar_t
+    libc.hdr.wchar_macros
+  )
+endif()
+
 add_fp_unittest(
   sprintf_test
   UNIT_TEST_ONLY
@@ -148,6 +158,7 @@ add_fp_unittest(
     libc.src.stdio.sprintf
     libc.src.__support.FPUtil.fp_bits
     libc.include.inttypes
+    ${wchar_deps}
   COMPILE_OPTIONS
     ${sprintf_test_copts}
 )
diff --git a/libc/test/src/stdio/sprintf_test.cpp b/libc/test/src/stdio/sprintf_test.cpp
index 42fdd59cf4d9c..83738793d1221 100644
--- a/libc/test/src/stdio/sprintf_test.cpp
+++ b/libc/test/src/stdio/sprintf_test.cpp
@@ -9,6 +9,11 @@
 #include "src/__support/macros/config.h"
 #include "src/stdio/sprintf.h"
 
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+#include "hdr/types/wchar_t.h"
+#include "hdr/wchar_macros.h"
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+
 #include "src/__support/FPUtil/FPBits.h"
 #include "src/__support/libc_errno.h"
 #include "test/UnitTest/RoundingModeUtils.h"
@@ -3475,3 +3480,41 @@ TEST(LlvmLibcSPrintfTest, IndexModeParsing) {
                    "why would u do this, this is such   a pain. %");
 }
 #endif // LIBC_COPT_PRINTF_DISABLE_INDEX_MODE
+
+#ifndef LIBC_COPT_PRINTF_DISABLE_WIDE
+TEST(LlvmLibcSprintfTest, WideCharConversion) {
+  char buff[16];
+  int written;
+
+  // Euro sign is a 3-byte UTF-8 character.
+  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 3);
+  ASSERT_STREQ(buff, "€");
+
+  // Euro sign left justified.
+  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%-4lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 6);
+  ASSERT_STREQ(buff, "€   ");
+
+  // Euro sign right justified.
+  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%4lc",
+                                     static_cast<wchar_t>(L'€'));
+  EXPECT_EQ(written, 6);
+  ASSERT_STREQ(buff, "   €");
+
+  // WEOF test.
+  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(WEOF));
+  EXPECT_EQ(written, -1);
+  ASSERT_ERRNO_FAILURE();
+
+  // Invalid wide character test
+  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+                                     static_cast<wchar_t>(0x12ffff));
+  EXPECT_EQ(written, -1);
+  ASSERT_ERRNO_FAILURE();
+}
+#endif // LIBC_COPT_PRINTF_DISABLE_WIDE
+

>From 74daf4ab9f9fa11fad86d339eb29ed5d1a5a8397 Mon Sep 17 00:00:00 2001
From: "shubh at DOE" <shubhp at mbm3a24.local>
Date: Fri, 19 Dec 2025 16:44:02 -0800
Subject: [PATCH 3/5] fix test

---
 libc/config/config.json                   |  4 ++++
 libc/config/windows/config.json           |  2 +-
 libc/src/stdio/printf_core/CMakeLists.txt |  3 ++-
 libc/test/src/stdio/sprintf_test.cpp      | 10 +++++-----
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/libc/config/config.json b/libc/config/config.json
index 1c11b9a86d8a4..a5254c66ea472 100644
--- a/libc/config/config.json
+++ b/libc/config/config.json
@@ -52,6 +52,10 @@
     "LIBC_CONF_PRINTF_RUNTIME_DISPATCH": {
       "value": true,
       "doc": "Use dynamic dispatch for the output mechanism to reduce code size."
+    },
+    "LIBC_CONF_PRINTF_DISABLE_WIDE": {
+      "value": false,
+      "doc": "Disable handling wide characters for printf and friends."
     }
   },
   "scanf": {
diff --git a/libc/config/windows/config.json b/libc/config/windows/config.json
index 6fab0387dbb5f..a7c94a174ac72 100644
--- a/libc/config/windows/config.json
+++ b/libc/config/windows/config.json
@@ -1,6 +1,6 @@
 {
     "printf": {
-      "LIBC_COPT_PRINTF_DISBALE_WIDE": {
+      "LIBC_CONF_PRINTF_DISBALE_WIDE": {
         "value": "true",
         "doc": "Disable handling wide characters for printf and friends."
       }
diff --git a/libc/src/stdio/printf_core/CMakeLists.txt b/libc/src/stdio/printf_core/CMakeLists.txt
index 7d4a6dc79ea8c..06d74937def17 100644
--- a/libc/src/stdio/printf_core/CMakeLists.txt
+++ b/libc/src/stdio/printf_core/CMakeLists.txt
@@ -43,8 +43,9 @@ if(NOT TARGET ${target_error_mapper})
     set(target_error_mapper libc.src.stdio.printf_core.generic.error_mapper)
 endif()
 
-if(LIBC_COPT_PRINTF_DISABLE_WIDE)
+if(LIBC_CONF_PRINTF_DISABLE_WIDE)
   set(wchar_deps "")
+  list(APPEND printf_config_copts "-DLIBC_COPT_PRINTF_DISABLE_WIDE")
 else()
   set(wchar_deps
     libc.hdr.types.wchar_t
diff --git a/libc/test/src/stdio/sprintf_test.cpp b/libc/test/src/stdio/sprintf_test.cpp
index 83738793d1221..ea8f0df7f4541 100644
--- a/libc/test/src/stdio/sprintf_test.cpp
+++ b/libc/test/src/stdio/sprintf_test.cpp
@@ -3487,31 +3487,31 @@ TEST(LlvmLibcSprintfTest, WideCharConversion) {
   int written;
 
   // Euro sign is a 3-byte UTF-8 character.
-  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+  written = LIBC_NAMESPACE::sprintf(buff, "%lc",
                                      static_cast<wchar_t>(L'€'));
   EXPECT_EQ(written, 3);
   ASSERT_STREQ(buff, "€");
 
   // Euro sign left justified.
-  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%-4lc",
+  written = LIBC_NAMESPACE::sprintf(buff, "%-4lc",
                                      static_cast<wchar_t>(L'€'));
   EXPECT_EQ(written, 6);
   ASSERT_STREQ(buff, "€   ");
 
   // Euro sign right justified.
-  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%4lc",
+  written = LIBC_NAMESPACE::sprintf(buff, "%4lc",
                                      static_cast<wchar_t>(L'€'));
   EXPECT_EQ(written, 6);
   ASSERT_STREQ(buff, "   €");
 
   // WEOF test.
-  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+  written = LIBC_NAMESPACE::sprintf(buff, "%lc",
                                      static_cast<wchar_t>(WEOF));
   EXPECT_EQ(written, -1);
   ASSERT_ERRNO_FAILURE();
 
   // Invalid wide character test
-  written = LIBC_NAMESPACE::sprintf(buff, sizeof(buff), "%lc",
+  written = LIBC_NAMESPACE::sprintf(buff, "%lc",
                                      static_cast<wchar_t>(0x12ffff));
   EXPECT_EQ(written, -1);
   ASSERT_ERRNO_FAILURE();

>From 9f49b8831a2828dcca19f95cbdc7dc0196dbfe45 Mon Sep 17 00:00:00 2001
From: "shubh at DOE" <shubhp at mbm3a24.local>
Date: Fri, 19 Dec 2025 16:51:22 -0800
Subject: [PATCH 4/5] fix small test change

---
 libc/test/src/stdio/sprintf_test.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/libc/test/src/stdio/sprintf_test.cpp b/libc/test/src/stdio/sprintf_test.cpp
index ea8f0df7f4541..4270e26eac6d5 100644
--- a/libc/test/src/stdio/sprintf_test.cpp
+++ b/libc/test/src/stdio/sprintf_test.cpp
@@ -17,6 +17,7 @@
 #include "src/__support/FPUtil/FPBits.h"
 #include "src/__support/libc_errno.h"
 #include "test/UnitTest/RoundingModeUtils.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
 #include "test/UnitTest/Test.h"
 #include <inttypes.h>
 

>From 763ca7ebd44c0be7403e4f33e4c11ff9eaa935c8 Mon Sep 17 00:00:00 2001
From: "shubh at DOE" <shubhp at mbm3a24.local>
Date: Sat, 20 Dec 2025 00:44:18 -0800
Subject: [PATCH 5/5] fix typo

---
 libc/config/windows/config.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libc/config/windows/config.json b/libc/config/windows/config.json
index a7c94a174ac72..75bb5e9debdde 100644
--- a/libc/config/windows/config.json
+++ b/libc/config/windows/config.json
@@ -1,6 +1,6 @@
 {
     "printf": {
-      "LIBC_CONF_PRINTF_DISBALE_WIDE": {
+      "LIBC_CONF_PRINTF_DISABLE_WIDE": {
         "value": "true",
         "doc": "Disable handling wide characters for printf and friends."
       }



More information about the libc-commits mailing list