Yesterday I wrote:
> Bug 2:
> 
> When the application outputs a string, that starts with a non-ASCII
> character, using the function fwrite(), the console shows no output,
> and the stream's error indicator gets set.
> 
> How to reproduce:
> 1. Use Windows 10 or 11. Switch it to Japanese as main language.
> 2. Use the attached program. In the dev environment:
>   $ gcc -Wall foo.c
> 3. In a cmd.exe console:
>   $ chcp 932
>   $ .\a
> Look at the output of the parts E and F.
> ...
> * Normal applications use fwrite() for binary I/O and fputs() or
>   [v][f]printf or similar for text I/O.

Well, actually Gnulib's vfprintf function override uses fwrite()
for text I/O:

lib/fzprintf.c:64:  if (fwrite (output, 1, len, fp) < len)
lib/vfzprintf.c:61:  if (fwrite (output, 1, len, fp) < len)

Therefore I'm adding this workaround.


2025-09-17  Bruno Haible  <[email protected]>

        stdio-h: Work around fwrite bug in msvcrt.
        Reported by 松延 英樹 <[email protected]> in
        <https://github.com/mlocati/gettext-iconv-windows/issues/52>.
        * lib/stdio.in.h (gl_consolesafe_fwrite): New declaration.
        (fwrite): When msvcrt is in use, use gl_consolesafe_fwrite.
        * lib/stdio-consolesafe.c: New file.
        * lib/stdio-write.c (fwrite): When msvcrt is in use, use
        gl_consolesafe_fwrite.
        * modules/stdio.h (Files): Add lib/stdio-consolesafe.c.
        (Depends-on): Add stdckdint-h.
        (configure.ac): Define condition GL_COND_OBJ_STDIO_CONSOLESAFE.
        (Makefile.am): Arrange to compile stdio-consolesafe.c.
        * doc/posix-functions/fwrite.texi: Document the workaround.

From daafafb5943a7f992e111be2e2897a9649a05b32 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Wed, 17 Sep 2025 08:38:14 +0200
Subject: [PATCH 1/2] stdio-h: Work around fwrite bug in msvcrt.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reported by ?????? ?????? <[email protected]> in
<https://github.com/mlocati/gettext-iconv-windows/issues/52>.

* lib/stdio.in.h (gl_consolesafe_fwrite): New declaration.
(fwrite): When msvcrt is in use, use gl_consolesafe_fwrite.
* lib/stdio-consolesafe.c: New file.
* lib/stdio-write.c (fwrite): When msvcrt is in use, use
gl_consolesafe_fwrite.
* modules/stdio.h (Files): Add lib/stdio-consolesafe.c.
(Depends-on): Add stdckdint-h.
(configure.ac): Define condition GL_COND_OBJ_STDIO_CONSOLESAFE.
(Makefile.am): Arrange to compile stdio-consolesafe.c.
* doc/posix-functions/fwrite.texi: Document the workaround.
---
 ChangeLog                       | 16 +++++++
 doc/posix-functions/fwrite.texi | 16 ++++---
 lib/stdio-consolesafe.c         | 74 +++++++++++++++++++++++++++++++++
 lib/stdio-write.c               |  3 ++
 lib/stdio.in.h                  | 13 ++++++
 modules/stdio-h                 | 17 ++++++++
 6 files changed, 133 insertions(+), 6 deletions(-)
 create mode 100644 lib/stdio-consolesafe.c

diff --git a/ChangeLog b/ChangeLog
index ef87fda436..03b2f5bc3b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2025-09-17  Bruno Haible  <[email protected]>
+
+	stdio-h: Work around fwrite bug in msvcrt.
+	Reported by ?????? ?????? <[email protected]> in
+	<https://github.com/mlocati/gettext-iconv-windows/issues/52>.
+	* lib/stdio.in.h (gl_consolesafe_fwrite): New declaration.
+	(fwrite): When msvcrt is in use, use gl_consolesafe_fwrite.
+	* lib/stdio-consolesafe.c: New file.
+	* lib/stdio-write.c (fwrite): When msvcrt is in use, use
+	gl_consolesafe_fwrite.
+	* modules/stdio.h (Files): Add lib/stdio-consolesafe.c.
+	(Depends-on): Add stdckdint-h.
+	(configure.ac): Define condition GL_COND_OBJ_STDIO_CONSOLESAFE.
+	(Makefile.am): Arrange to compile stdio-consolesafe.c.
+	* doc/posix-functions/fwrite.texi: Document the workaround.
+
 2025-09-16  Bruno Haible  <[email protected]>
 
 	strerror_r-posix: Fix truncation code (regression today).
diff --git a/doc/posix-functions/fwrite.texi b/doc/posix-functions/fwrite.texi
index 3922a9f1db..3f105befae 100644
--- a/doc/posix-functions/fwrite.texi
+++ b/doc/posix-functions/fwrite.texi
@@ -9,6 +9,16 @@
 @mindex nonblocking
 @mindex sigpipe
 
+Portability problems fixed by Gnulib module @code{stdio-h}:
+@itemize
+@item
+This function fails and produces no output
+when the argument string starts with a non-ASCII character in double-byte encoding,
+corresponding to the locale, on some platforms:
+mingw in combination with msvcrt,
+when the output goes to a Windows console.
+@end itemize
+
 Portability problems fixed by Gnulib module @code{stdio-h}, together with module @code{nonblocking}:
 @itemize
 @item
@@ -32,12 +42,6 @@
 On Windows platforms (excluding Cygwin), this function does not set @code{errno}
 upon failure.
 @item
-This function fails and produces no output
-when the argument string starts with a non-ASCII character in double-byte encoding,
-corresponding to the locale, on some platforms:
-mingw in combination with msvcrt,
-when the output goes to a Windows console.
-@item
 On some platforms, this function does not set @code{errno} or the
 stream error indicator on attempts to write to a read-only stream:
 Cygwin 1.7.9.
diff --git a/lib/stdio-consolesafe.c b/lib/stdio-consolesafe.c
new file mode 100644
index 0000000000..12a79fb7c3
--- /dev/null
+++ b/lib/stdio-consolesafe.c
@@ -0,0 +1,74 @@
+/* msvcrt workarounds.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as
+   published by the Free Software Foundation; either version 2.1 of the
+   License, or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include <stdio.h>
+
+#include <stdckdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Outputs N bytes starting at S to FP.
+   These N bytes are known to be followed by a NUL.
+   Finally frees the string at S.
+   Returns the number of written bytes.  */
+static size_t
+workaround_fwrite0 (char *s, size_t n, FILE *fp)
+{
+  const char *ptr = s;
+  /* Use fputs instead of fwrite, which is buggy in msvcrt.  */
+  size_t written = 0;
+  while (n > 0)
+    {
+      size_t l = strlen (ptr); /* 0 <= l <= n */
+      if (l > 0)
+        {
+          if (fputs (ptr, fp) == EOF)
+            break;
+          written += l;
+          n -= l;
+        }
+      if (n == 0)
+        break;
+      if (fputc ('\0', fp) == EOF)
+        break;
+      written++;
+      n--;
+      ptr += l + 1;
+    }
+  free (s);
+  return written;
+}
+
+size_t
+gl_consolesafe_fwrite (const void *ptr, size_t size, size_t nmemb, FILE *fp)
+{
+  size_t nbytes;
+  if (ckd_mul (&nbytes, size, nmemb) || nbytes == 0)
+    /* Overflow, or nothing to do.  */
+    return 0;
+  char *tmp = malloc (nbytes + 1);
+  if (tmp == NULL)
+    return 0;
+  memcpy (tmp, ptr, nbytes);
+  tmp[nbytes] = '\0';
+  size_t written = workaround_fwrite0 (tmp, nbytes, fp);
+  return written / size;
+}
+
+
diff --git a/lib/stdio-write.c b/lib/stdio-write.c
index b5073ad229..1115111eda 100644
--- a/lib/stdio-write.c
+++ b/lib/stdio-write.c
@@ -198,6 +198,9 @@ puts (const char *string)
 size_t
 fwrite (const void *ptr, size_t s, size_t n, FILE *stream)
 #undef fwrite
+#if (defined _WIN32 && !defined __CYGWIN__) && !defined _UCRT
+# define fwrite gl_consolesafe_fwrite
+#endif
 {
   CALL_WITH_SIGPIPE_EMULATION (size_t, fwrite (ptr, s, n, stream), ret < n)
 }
diff --git a/lib/stdio.in.h b/lib/stdio.in.h
index 5370583470..08db0290cd 100644
--- a/lib/stdio.in.h
+++ b/lib/stdio.in.h
@@ -271,6 +271,14 @@
 #endif
 
 
+#if (defined _WIN32 && !defined __CYGWIN__) && !defined _UCRT
+/* Workarounds against msvcrt bugs.  */
+_GL_FUNCDECL_SYS (gl_consolesafe_fwrite, size_t,
+                  (const void *ptr, size_t size, size_t nmemb, FILE *fp),
+                  _GL_ARG_NONNULL ((1, 4)));
+#endif
+
+
 #if @GNULIB_DZPRINTF@
 /* Prints formatted output to file descriptor FD.
    Returns the number of bytes written to the file descriptor.  Upon
@@ -961,6 +969,11 @@ _GL_EXTERN_C size_t __REDIRECT (rpl_fwrite_unlocked,
 # if __GLIBC__ >= 2
 _GL_CXXALIASWARN (fwrite);
 # endif
+#elif (defined _WIN32 && !defined __CYGWIN__) && !defined _UCRT
+# if !(defined __cplusplus && defined GNULIB_NAMESPACE)
+#  undef fwrite
+#  define fwrite gl_consolesafe_fwrite
+# endif
 #endif
 
 #if @GNULIB_GETC@
diff --git a/modules/stdio-h b/modules/stdio-h
index 6190bb95aa..02a08688ec 100644
--- a/modules/stdio-h
+++ b/modules/stdio-h
@@ -3,6 +3,7 @@ A GNU-like <stdio.h>.
 
 Files:
 lib/stdio.in.h
+lib/stdio-consolesafe.c
 lib/stdio-read.c
 lib/stdio-write.c
 m4/stdio_h.m4
@@ -18,6 +19,7 @@ snippet/warn-on-use
 ssize_t
 stddef-h
 sys_types-h
+stdckdint-h
 
 configure.ac-early:
 gl_STDIO_H_EARLY
@@ -26,6 +28,18 @@ configure.ac:
 gl_STDIO_H
 gl_STDIO_H_REQUIRE_DEFAULTS
 AC_PROG_MKDIR_P
+USES_MSVCRT=0
+case "$host_os" in
+  mingw* | windows*)
+    AC_EGREP_CPP([Special], [
+#ifndef _UCRT
+ Special
+#endif
+      ],
+      [USES_MSVCRT=1])
+    ;;
+esac
+gl_CONDITIONAL([GL_COND_OBJ_STDIO_CONSOLESAFE], [test $USES_MSVCRT = 1])
 gl_CONDITIONAL([GL_COND_OBJ_STDIO_READ], [test $REPLACE_STDIO_READ_FUNCS = 1])
 gl_CONDITIONAL([GL_COND_OBJ_STDIO_WRITE], [test $REPLACE_STDIO_WRITE_FUNCS = 1])
 
@@ -204,6 +218,9 @@ stdio.h: stdio.in.h $(top_builddir)/config.status $(CXXDEFS_H) $(ARG_NONNULL_H)
 	$(AM_V_at)mv $@-t3 $@
 MOSTLYCLEANFILES += stdio.h stdio.h-t1 stdio.h-t2 stdio.h-t3
 
+if GL_COND_OBJ_STDIO_CONSOLESAFE
+lib_SOURCES += stdio-consolesafe.c
+endif
 if GL_COND_OBJ_STDIO_READ
 lib_SOURCES += stdio-read.c
 endif
-- 
2.50.1

Reply via email to