[libc++] Fix checks for terminal and flushes in std::print() (#70321)

The check whether a stream is associated with a terminal or not and the
flushing of the stream in `std::print()` is needed only on Windows.
Additionally, the correct flush should be used. When `std::print` is
called with a C stream, `std::fflush()` should be used. When it is
called with C++ `ostream`, `ostream::flush()` should be called.

Because POSIX does not have a separate Unicode API for terminal output,
checking for terminal (`isatty`) and flushing is not needed at all.
Moreover, `isatty` has noticeable performance cost.

See also https://wg21.link/LWG4044.

Fixes #70142
This commit is contained in:
Dimitrij Mijoski 2026-03-12 10:58:36 +01:00 committed by GitHub
parent 0c4eaba2f9
commit 1616fbaccf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 59 additions and 173 deletions

View File

@ -138,7 +138,6 @@ jobs:
'generic-no-experimental',
'generic-no-filesystem',
'generic-no-localization',
'generic-no-terminal',
'generic-no-random_device',
'generic-no-threads',
'generic-no-tzdb',

View File

@ -106,8 +106,6 @@ option(LIBCXX_ENABLE_UNICODE
"Whether to include support for Unicode in the library. Disabling Unicode can
be useful when porting to platforms that don't support UTF-8 encoding (e.g.
embedded)." ON)
option(LIBCXX_HAS_TERMINAL_AVAILABLE
"Build libc++ with support for checking whether a stream is a terminal." ON)
option(LIBCXX_ENABLE_WIDE_CHARACTERS
"Whether to include support for wide characters in the library. Disabling
wide character support can be useful when porting to platforms that don't
@ -750,7 +748,6 @@ config_define(${LIBCXX_ABI_FORCE_ITANIUM} _LIBCPP_ABI_FORCE_ITANIUM)
config_define(${LIBCXX_ABI_FORCE_MICROSOFT} _LIBCPP_ABI_FORCE_MICROSOFT)
config_define(${LIBCXX_ENABLE_THREADS} _LIBCPP_HAS_THREADS)
config_define(${LIBCXX_ENABLE_MONOTONIC_CLOCK} _LIBCPP_HAS_MONOTONIC_CLOCK)
config_define(${LIBCXX_HAS_TERMINAL_AVAILABLE} _LIBCPP_HAS_TERMINAL)
if (NOT LIBCXX_TYPEINFO_COMPARISON_IMPLEMENTATION STREQUAL "default")
config_define("${LIBCXX_TYPEINFO_COMPARISON_IMPLEMENTATION}" _LIBCPP_TYPEINFO_COMPARISON_IMPLEMENTATION)
endif()

View File

@ -1,5 +0,0 @@
set(LIBCXX_HAS_TERMINAL_AVAILABLE OFF CACHE BOOL "")
# Speed up the CI
set(LIBCXX_TEST_PARAMS "enable_modules=clang" CACHE STRING "")
set(LIBCXXABI_TEST_PARAMS "${LIBCXX_TEST_PARAMS}" CACHE STRING "")

View File

@ -15,7 +15,6 @@
#cmakedefine01 _LIBCPP_ABI_FORCE_MICROSOFT
#cmakedefine01 _LIBCPP_HAS_THREADS
#cmakedefine01 _LIBCPP_HAS_MONOTONIC_CLOCK
#cmakedefine01 _LIBCPP_HAS_TERMINAL
#cmakedefine01 _LIBCPP_HAS_MUSL_LIBC
#cmakedefine01 _LIBCPP_HAS_THREAD_API_PTHREAD
#cmakedefine01 _LIBCPP_HAS_THREAD_API_EXTERNAL

View File

@ -255,12 +255,6 @@
#define _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION _LIBCPP_INTRODUCED_IN_LLVM_18
#define _LIBCPP_AVAILABILITY_INIT_PRIMARY_EXCEPTION _LIBCPP_INTRODUCED_IN_LLVM_18_ATTRIBUTE
// This controls the availability of C++23 <print>, which
// has a dependency on the built library (it needs access to
// the underlying buffer types of std::cout, std::cerr, and std::clog.
#define _LIBCPP_AVAILABILITY_HAS_PRINT _LIBCPP_INTRODUCED_IN_LLVM_18
#define _LIBCPP_AVAILABILITY_PRINT _LIBCPP_INTRODUCED_IN_LLVM_18_ATTRIBUTE
// This controls the availability of the C++17 std::pmr library,
// which is implemented in large part in the built library.
//

View File

@ -87,7 +87,7 @@ _LIBCPP_EXPORTED_FROM_ABI FILE* __get_ostream_file(ostream& __os);
# if _LIBCPP_HAS_UNICODE
template <class = void> // TODO PRINT template or availability markup fires too eagerly (http://llvm.org/PR61563).
_LIBCPP_HIDE_FROM_ABI void __vprint_unicode(ostream& __os, string_view __fmt, format_args __args, bool __write_nl) {
# if _LIBCPP_AVAILABILITY_HAS_PRINT == 0
# ifndef _LIBCPP_WIN32API
return std::__vprint_nonunicode(__os, __fmt, __args, __write_nl);
# else
FILE* __file = std::__get_ostream_file(__os);
@ -110,10 +110,8 @@ _LIBCPP_HIDE_FROM_ABI void __vprint_unicode(ostream& __os, string_view __fmt, fo
# endif // _LIBCPP_HAS_EXCEPTIONS
ostream::sentry __s(__os);
if (__s) {
# ifndef _LIBCPP_WIN32API
__print::__vprint_unicode_posix(__file, __fmt, __args, __write_nl, true);
# elif _LIBCPP_HAS_WIDE_CHARACTERS
__print::__vprint_unicode_windows(__file, __fmt, __args, __write_nl, true);
# if _LIBCPP_HAS_WIDE_CHARACTERS
__print::__vprint_unicode_windows(__file, __fmt, __args, __write_nl);
# else
# error "Windows builds with wchar_t disabled are not supported."
# endif
@ -124,7 +122,7 @@ _LIBCPP_HIDE_FROM_ABI void __vprint_unicode(ostream& __os, string_view __fmt, fo
__os.__set_badbit_and_consider_rethrow();
}
# endif // _LIBCPP_HAS_EXCEPTIONS
# endif // _LIBCPP_AVAILABILITY_HAS_PRINT
# endif // _LIBCPP_WIN32API
}
template <class = void> // TODO PRINT template or availability markup fires too eagerly (http://llvm.org/PR61563).

View File

@ -69,9 +69,7 @@ _LIBCPP_EXPORTED_FROM_ABI bool __is_windows_terminal(FILE* __stream);
// Note the function is only implemented on the Windows platform.
_LIBCPP_EXPORTED_FROM_ABI void __write_to_windows_console(FILE* __stream, wstring_view __view);
# endif // _LIBCPP_HAS_WIDE_CHARACTERS
# elif __has_include(<unistd.h>)
_LIBCPP_EXPORTED_FROM_ABI bool __is_posix_terminal(FILE* __stream);
# endif // _LIBCPP_WIN32API
# endif // _LIBCPP_WIN32API
# if _LIBCPP_STD_VER >= 23
@ -197,21 +195,17 @@ inline constexpr bool __use_unicode_execution_charset = _MSVC_EXECUTION_CHARACTE
inline constexpr bool __use_unicode_execution_charset = true;
# endif
# ifdef _LIBCPP_WIN32API
_LIBCPP_HIDE_FROM_ABI inline bool __is_terminal([[maybe_unused]] FILE* __stream) {
// The macro _LIBCPP_TESTING_PRINT_IS_TERMINAL is used to change
// the behavior in the test. This is not part of the public API.
# ifdef _LIBCPP_TESTING_PRINT_IS_TERMINAL
# ifdef _LIBCPP_TESTING_PRINT_IS_TERMINAL
return _LIBCPP_TESTING_PRINT_IS_TERMINAL(__stream);
# elif _LIBCPP_AVAILABILITY_HAS_PRINT == 0 || !_LIBCPP_HAS_TERMINAL
return false;
# elif defined(_LIBCPP_WIN32API)
# else
return std::__is_windows_terminal(__stream);
# elif __has_include(<unistd.h>)
return std::__is_posix_terminal(__stream);
# else
# error "Provide a way to determine whether a FILE* is a terminal"
# endif
# endif
}
# endif // _LIBCPP_WIN32API
template <class = void> // TODO PRINT template or availability markup fires too eagerly (http://llvm.org/PR61563).
_LIBCPP_HIDE_FROM_ABI inline void
@ -236,26 +230,11 @@ __vprint_nonunicode(FILE* __stream, string_view __fmt, format_args __args, bool
// terminal when the output is redirected. Typically during testing the
// output is redirected to be able to capture it. This makes it hard to
// test this code path.
template <class = void> // TODO PRINT template or availability markup fires too eagerly (http://llvm.org/PR61563).
_LIBCPP_HIDE_FROM_ABI inline void
__vprint_unicode_posix(FILE* __stream, string_view __fmt, format_args __args, bool __write_nl, bool __is_terminal) {
// TODO PRINT Should flush errors throw too?
if (__is_terminal)
std::fflush(__stream);
__print::__vprint_nonunicode(__stream, __fmt, __args, __write_nl);
}
# if _LIBCPP_HAS_WIDE_CHARACTERS
template <class = void> // TODO PRINT template or availability markup fires too eagerly (http://llvm.org/PR61563).
_LIBCPP_HIDE_FROM_ABI inline void
__vprint_unicode_windows(FILE* __stream, string_view __fmt, format_args __args, bool __write_nl, bool __is_terminal) {
if (!__is_terminal)
return __print::__vprint_nonunicode(__stream, __fmt, __args, __write_nl);
// TODO PRINT Should flush errors throw too?
std::fflush(__stream);
__vprint_unicode_windows([[maybe_unused]] FILE* __stream, string_view __fmt, format_args __args, bool __write_nl) {
string __str = std::vformat(__fmt, __args);
// UTF-16 uses the same number or less code units than UTF-8.
// However the size of the code unit is 16 bits instead of 8 bits.
@ -316,9 +295,15 @@ __vprint_unicode([[maybe_unused]] FILE* __stream,
// Windows there is a different API. This API requires transcoding.
# ifndef _LIBCPP_WIN32API
__print::__vprint_unicode_posix(__stream, __fmt, __args, __write_nl, __print::__is_terminal(__stream));
__print::__vprint_nonunicode(__stream, __fmt, __args, __write_nl);
# elif _LIBCPP_HAS_WIDE_CHARACTERS
__print::__vprint_unicode_windows(__stream, __fmt, __args, __write_nl, __print::__is_terminal(__stream));
if (__print::__is_terminal(__stream)) {
// TODO PRINT Should flush errors throw too?
std::fflush(__stream);
__print::__vprint_unicode_windows(__stream, __fmt, __args, __write_nl);
} else {
__print::__vprint_nonunicode(__stream, __fmt, __args, __write_nl);
}
# else
# error "Windows builds with wchar_t disabled are not supported."
# endif

View File

@ -64,9 +64,12 @@ __write_to_windows_console([[maybe_unused]] FILE* __stream, [[maybe_unused]] wst
}
# endif // _LIBCPP_HAS_WIDE_CHARACTERS
#elif defined(HAS_FILENO_AND_ISATTY) // !_LIBCPP_WIN32API
#elif defined(HAS_FILENO_AND_ISATTY) && _LIBCPP_AVAILABILITY_MINIMUM_HEADER_VERSION < 23 // !_LIBCPP_WIN32API
_LIBCPP_DIAGNOSTIC_PUSH
_LIBCPP_CLANG_DIAGNOSTIC_IGNORED("-Wmissing-prototypes")
_LIBCPP_EXPORTED_FROM_ABI bool __is_posix_terminal(FILE* __stream) { return isatty(fileno(__stream)); }
_LIBCPP_DIAGNOSTIC_POP
#endif
_LIBCPP_END_NAMESPACE_STD

View File

@ -13,13 +13,8 @@
// XFAIL: availability-fp_to_chars-missing
// When std::print is unavailable, we don't rely on an implementation of
// std::__is_terminal and we always assume a non-unicode and non-terminal
// output.
// XFAIL: availability-print-missing
// Clang modules do not work with the definiton of _LIBCPP_TESTING_PRINT_IS_TERMINAL
// XFAIL: clang-modules-build
// ADDITIONAL_COMPILE_FLAGS: -fno-modules
// <ostream>
// Tests the implementation of
@ -81,14 +76,22 @@ static void test_is_terminal_file_stream() {
assert(stream.is_open());
assert(stream.good());
std::print(stream, "test");
#ifdef _WIN32
assert(is_terminal_calls == 1);
#else
assert(is_terminal_calls == 0);
#endif
}
{
std::ofstream stream(filename);
assert(stream.is_open());
assert(stream.good());
std::print(stream, "test");
#ifdef _WIN32
assert(is_terminal_calls == 2);
#else
assert(is_terminal_calls == 0);
#endif
}
}
@ -105,7 +108,11 @@ static void test_is_terminal_rdbuf_derived_from_filebuf() {
std::ostream stream(&buf);
std::print(stream, "test");
#ifdef _WIN32
assert(is_terminal_calls == 1);
#else
assert(is_terminal_calls == 0);
#endif
}
// When the stream is cout, clog, or cerr, its FILE* may be a terminal. Validate
@ -115,15 +122,27 @@ static void test_is_terminal_std_cout_cerr_clog() {
is_terminal_result = false;
{
std::print(std::cout, "test");
#ifdef _WIN32
assert(is_terminal_calls == 1);
#else
assert(is_terminal_calls == 0);
#endif
}
{
std::print(std::cerr, "test");
#ifdef _WIN32
assert(is_terminal_calls == 2);
#else
assert(is_terminal_calls == 0);
#endif
}
{
std::print(std::clog, "test");
#ifdef _WIN32
assert(is_terminal_calls == 3);
#else
assert(is_terminal_calls == 0);
#endif
}
}
@ -156,7 +175,11 @@ static void test_is_terminal_is_flushed() {
// A terminal sync is called.
is_terminal_result = true;
std::print(stream, "");
#ifdef _WIN32
assert(buf.sync_calls == 1); // only called from the destructor of the sentry
#else
assert(buf.sync_calls == 0);
#endif
}
int main(int, char**) {

View File

@ -1,79 +0,0 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
// UNSUPPORTED: no-filesystem
// UNSUPPORTED: libcpp-has-no-unicode
// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
// XFAIL: availability-fp_to_chars-missing
// fmemopen is available starting in Android M (API 23)
// XFAIL: target={{.+}}-android{{(eabi)?(21|22)}}
// REQUIRES: has-unix-headers
// <print>
// Tests the implementation of
// void __print::__vprint_unicode_posix(FILE* __stream, string_view __fmt,
// format_args __args, bool __write_nl,
// bool __is_terminal);
//
// In the library when the stdout is redirected to a file it is no
// longer considered a terminal and the special terminal handling is no
// longer executed. By testing this function we can "force" emulate a
// terminal.
// Note __write_nl is tested by the public API.
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdio>
#include <print>
#include "test_macros.h"
int main(int, char**) {
std::array<char, 100> buffer;
std::ranges::fill(buffer, '*');
FILE* file = fmemopen(buffer.data(), buffer.size(), "wb");
assert(file);
// Test the file is buffered.
std::fprintf(file, "Hello");
assert(std::ftell(file) == 5);
#if defined(TEST_HAS_GLIBC) && \
!(__has_feature(address_sanitizer) || __has_feature(thread_sanitizer) || __has_feature(memory_sanitizer))
assert(std::ranges::all_of(buffer, [](char c) { return c == '*'; }));
#endif
// Test writing to a "non-terminal" stream does not flush.
std::__print::__vprint_unicode_posix(file, " world", std::make_format_args(), false, false);
assert(std::ftell(file) == 11);
#if defined(TEST_HAS_GLIBC) && \
!(__has_feature(address_sanitizer) || __has_feature(thread_sanitizer) || __has_feature(memory_sanitizer))
assert(std::ranges::all_of(buffer, [](char c) { return c == '*'; }));
#endif
// Test writing to a "terminal" stream flushes before writing.
std::__print::__vprint_unicode_posix(file, "!", std::make_format_args(), false, true);
assert(std::ftell(file) == 12);
assert(std::string_view(buffer.data(), buffer.data() + 11) == "Hello world");
#if defined(TEST_HAS_GLIBC)
// glibc does not flush after a write.
assert(buffer[11] != '!');
#endif
// Test everything is written when closing the stream.
std::fclose(file);
assert(std::string_view(buffer.data(), buffer.data() + 12) == "Hello world!");
return 0;
}

View File

@ -13,7 +13,7 @@
// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
// Clang modules do not work with the definiton of _LIBCPP_TESTING_PRINT_WRITE_TO_WINDOWS_CONSOLE_FUNCTION
// XFAIL: clang-modules-build
// ADDITIONAL_COMPILE_FLAGS: -fno-modules
// XFAIL: availability-fp_to_chars-missing
@ -21,8 +21,7 @@
// Tests the implementation of
// void __print::__vprint_unicode_windows(FILE* __stream, string_view __fmt,
// format_args __args, bool __write_nl,
// bool __is_terminal);
// format_args __args, bool __write_nl);
//
// In the library when the stdout is redirected to a file it is no
// longer considered a terminal and the special terminal handling is no
@ -62,40 +61,19 @@ static void test_basics() {
FILE* file = std::fopen(filename.c_str(), "wb");
assert(file);
// Test writing to a "non-terminal" stream does not call WriteConsoleW.
std::__print::__vprint_unicode_windows(file, "Hello", std::make_format_args(), false, false);
assert(std::ftell(file) == 5);
// It's not possible to reliably test whether writing to a "terminal" stream
// flushes before writing. Testing flushing a closed stream worked on some
// platforms, but was unreliable.
calling = true;
std::__print::__vprint_unicode_windows(file, " world", std::make_format_args(), false, true);
std::__print::__vprint_unicode_windows(file, " world", std::make_format_args(), false);
}
// When the output is a file the data is written as-is.
// When the output is a "terminal" invalid UTF-8 input is flagged.
// Invalid UTF-8 input is flagged.
static void test(std::wstring_view output, std::string_view input) {
// *** File ***
FILE* file = std::fopen(filename.c_str(), "wb");
assert(file);
std::__print::__vprint_unicode_windows(file, input, std::make_format_args(), false, false);
assert(std::ftell(file) == static_cast<long>(input.size()));
std::fclose(file);
file = std::fopen(filename.c_str(), "rb");
assert(file);
std::vector<char> buffer(input.size());
size_t read = fread(buffer.data(), 1, buffer.size(), file);
assert(read == input.size());
assert(input == std::string_view(buffer.begin(), buffer.end()));
std::fclose(file);
// *** Terminal ***
expected = output;
std::__print::__vprint_unicode_windows(file, input, std::make_format_args(), false, true);
std::__print::__vprint_unicode_windows(file, input, std::make_format_args(), false);
assert(std::ftell(file) == 0);
std::fclose(file);
}
static void test() {

View File

@ -486,11 +486,6 @@ generic-no-localization)
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-no-localization.cmake"
check-runtimes
;;
generic-no-terminal)
clean
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-no-terminal.cmake"
check-runtimes
;;
generic-no-unicode)
clean
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-no-unicode.cmake"

View File

@ -67,7 +67,6 @@ inverted_macros = {
"_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS": "libcpp-has-no-availability-markup",
"_LIBCPP_HAS_RANDOM_DEVICE": "no-random-device",
"_LIBCPP_HAS_UNICODE": "libcpp-has-no-unicode",
"_LIBCPP_HAS_TERMINAL": "no-terminal",
}
for macro, feature in inverted_macros.items():
features.append(