[libc-commits] [libc] [libc] Implement perror (PR #143624)

Michael Jones via libc-commits libc-commits at lists.llvm.org
Wed Jun 11 13:16:35 PDT 2025


https://github.com/michaelrj-google updated https://github.com/llvm/llvm-project/pull/143624

>From e72090c3a0435d1e545b02f28eb6d51c1bfb7788 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Tue, 10 Jun 2025 15:40:51 -0700
Subject: [PATCH 1/3] [libc] Implement perror

The perror function writes an error message directly to stderr. This
patch adds an implementation, tests, and header generation details.
---
 libc/config/linux/x86_64/entrypoints.txt |  1 +
 libc/include/stdio.yaml                  |  6 ++
 libc/src/stdio/CMakeLists.txt            |  1 +
 libc/src/stdio/generic/CMakeLists.txt    | 15 ++++
 libc/src/stdio/generic/perror.cpp        | 87 ++++++++++++++++++++++++
 libc/src/stdio/perror.h                  | 20 ++++++
 libc/test/src/stdio/CMakeLists.txt       | 12 ++++
 libc/test/src/stdio/perror_test.cpp      | 32 +++++++++
 8 files changed, 174 insertions(+)
 create mode 100644 libc/src/stdio/generic/perror.cpp
 create mode 100644 libc/src/stdio/perror.h
 create mode 100644 libc/test/src/stdio/perror_test.cpp

diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index 959bdbf08dbea..ff84069384e86 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1113,6 +1113,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.perror
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/include/stdio.yaml b/libc/include/stdio.yaml
index 2619984cca264..63acbb0460c11 100644
--- a/libc/include/stdio.yaml
+++ b/libc/include/stdio.yaml
@@ -247,6 +247,12 @@ functions:
       - POSIX
     return_type: int
     arguments: []
+  - name: perror
+    standards:
+      - stdc
+    return_type: void
+    arguments:
+      - type: const char *
   - name: printf
     standards:
       - stdc
diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt
index 63f6ed8a11f1d..b0a6ef1e291b5 100644
--- a/libc/src/stdio/CMakeLists.txt
+++ b/libc/src/stdio/CMakeLists.txt
@@ -221,6 +221,7 @@ add_stdio_entrypoint_object(fopen)
 add_stdio_entrypoint_object(fclose)
 add_stdio_entrypoint_object(fread_unlocked)
 add_stdio_entrypoint_object(fread)
+add_stdio_entrypoint_object(perror)
 add_stdio_entrypoint_object(puts)
 add_stdio_entrypoint_object(fputs)
 add_stdio_entrypoint_object(fwrite_unlocked)
diff --git a/libc/src/stdio/generic/CMakeLists.txt b/libc/src/stdio/generic/CMakeLists.txt
index e1f4ed5c19497..6361822b61999 100644
--- a/libc/src/stdio/generic/CMakeLists.txt
+++ b/libc/src/stdio/generic/CMakeLists.txt
@@ -206,6 +206,21 @@ add_generic_entrypoint_object(
     libc.src.__support.File.platform_file
 )
 
+add_generic_entrypoint_object(
+  perror
+  SRCS
+    perror.cpp
+  HDRS
+    ../perror.h
+  DEPENDS
+    libc.src.errno.errno
+    libc.src.__support.StringUtil.error_to_string
+    libc.src.__support.CPP.string_view
+    libc.src.__support.File.file
+    libc.src.__support.File.platform_file
+    libc.src.__support.File.platform_stderr
+)
+
 add_generic_entrypoint_object(
   fputs
   SRCS
diff --git a/libc/src/stdio/generic/perror.cpp b/libc/src/stdio/generic/perror.cpp
new file mode 100644
index 0000000000000..ce2424810f2c5
--- /dev/null
+++ b/libc/src/stdio/generic/perror.cpp
@@ -0,0 +1,87 @@
+//===-- Implementation of perror ------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/perror.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/File/file.h"
+#include "src/__support/StringUtil/error_to_string.h"
+#include "src/__support/macros/config.h"
+#include "src/errno/libc_errno.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+namespace {
+
+// TODO: this is copied from `puts`, it should be moved to a shared utility.
+//  Simple helper to unlock the file once destroyed.
+struct ScopedLock {
+  ScopedLock(LIBC_NAMESPACE::File *stream) : stream(stream) { stream->lock(); }
+  ~ScopedLock() { stream->unlock(); }
+
+private:
+  LIBC_NAMESPACE::File *stream;
+};
+
+int write_out(cpp::string_view str_view, File *f) {
+  if (str_view.size() > 0) {
+    auto result = f->write_unlocked(str_view.data(), str_view.size());
+    if (result.has_error())
+      return result.error;
+  }
+  return 0;
+}
+
+} // namespace
+
+// TODO: this seems like there should be some sort of queue system to
+// deduplicate this code.
+LLVM_LIBC_FUNCTION(void, perror, (const char *str)) {
+  const char empty_str[1] = {'\0'};
+  if (str == nullptr)
+    str = empty_str;
+  cpp::string_view str_view(str);
+
+  auto err_str = get_error_string(libc_errno);
+
+  // We need to lock the stream to ensure the newline is always appended.
+  ScopedLock lock(LIBC_NAMESPACE::stderr);
+  int write_err;
+
+  // FORMAT:
+  // if str != nullptr and doesn't start with a null byte:
+  //   "[str]: [strerror(errno)]\n"
+  // else
+  //   "[strerror(errno)]\n"
+  if (str_view.size() > 0) {
+    write_err = write_out(str_view, LIBC_NAMESPACE::stderr);
+    if (write_err != 0) {
+      libc_errno = write_err;
+      return;
+    }
+
+    write_err = write_out(": ", LIBC_NAMESPACE::stderr);
+    if (write_err != 0) {
+      libc_errno = write_err;
+      return;
+    }
+  }
+
+  write_err = write_out(err_str, LIBC_NAMESPACE::stderr);
+  if (write_err != 0) {
+    libc_errno = write_err;
+    return;
+  }
+
+  write_err = write_out("\n", LIBC_NAMESPACE::stderr);
+  if (write_err != 0) {
+    libc_errno = write_err;
+    return;
+  }
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/perror.h b/libc/src/stdio/perror.h
new file mode 100644
index 0000000000000..bf8d0af1df5d7
--- /dev/null
+++ b/libc/src/stdio/perror.h
@@ -0,0 +1,20 @@
+//===-- Implementation header of perror -------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_PERROR_H
+#define LLVM_LIBC_SRC_STDIO_PERROR_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+void perror(const char *s);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_PERROR_H
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index 01904a30504ed..ce2171f19597b 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -357,6 +357,18 @@ add_libc_test(
     libc.src.stdio.puts
 )
 
+add_libc_test(
+  perror_test
+  HERMETIC_TEST_ONLY # writes to libc's stderr
+  SUITE
+    libc_stdio_unittests
+  SRCS
+    perror_test.cpp
+  DEPENDS
+    libc.src.stdio.perror
+    libc.src.errno.errno
+)
+
 add_libc_test(
   fputs_test
   HERMETIC_TEST_ONLY # writes to libc's stdout and stderr
diff --git a/libc/test/src/stdio/perror_test.cpp b/libc/test/src/stdio/perror_test.cpp
new file mode 100644
index 0000000000000..4ac0ccc2f2f14
--- /dev/null
+++ b/libc/test/src/stdio/perror_test.cpp
@@ -0,0 +1,32 @@
+//===-- Unittests for perror ---------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/perror.h"
+
+#include "src/errno/libc_errno.h"
+#include "test/UnitTest/Test.h"
+
+// The standard says perror prints directly to stderr and returns nothing. This
+// makes it rather difficult to test automatically.
+
+// TODO: figure out redirecting stderr so this test can check correctness.
+TEST(LlvmLibcPerrorTest, PrintOut) {
+  LIBC_NAMESPACE::libc_errno = 0;
+  constexpr char simple[] = "A simple string";
+  LIBC_NAMESPACE::perror(simple);
+
+  // stick to stdc errno values, specifically 0, EDOM, ERANGE, and EILSEQ.
+  LIBC_NAMESPACE::libc_errno = EDOM;
+  LIBC_NAMESPACE::perror("Print this and an error");
+
+  LIBC_NAMESPACE::libc_errno = EILSEQ;
+  LIBC_NAMESPACE::perror("\0 shouldn't print this.");
+
+  LIBC_NAMESPACE::libc_errno = ERANGE;
+  LIBC_NAMESPACE::perror(nullptr);
+}

>From af60e6fc6fa77ac39366dbda532df5fa28a99135 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Tue, 10 Jun 2025 16:18:37 -0700
Subject: [PATCH 2/3] clean up lock

---
 libc/src/stdio/generic/perror.cpp | 45 +++++++++++++------------------
 1 file changed, 19 insertions(+), 26 deletions(-)

diff --git a/libc/src/stdio/generic/perror.cpp b/libc/src/stdio/generic/perror.cpp
index ce2424810f2c5..2c39df55f8adc 100644
--- a/libc/src/stdio/generic/perror.cpp
+++ b/libc/src/stdio/generic/perror.cpp
@@ -15,18 +15,6 @@
 
 namespace LIBC_NAMESPACE_DECL {
 
-namespace {
-
-// TODO: this is copied from `puts`, it should be moved to a shared utility.
-//  Simple helper to unlock the file once destroyed.
-struct ScopedLock {
-  ScopedLock(LIBC_NAMESPACE::File *stream) : stream(stream) { stream->lock(); }
-  ~ScopedLock() { stream->unlock(); }
-
-private:
-  LIBC_NAMESPACE::File *stream;
-};
-
 int write_out(cpp::string_view str_view, File *f) {
   if (str_view.size() > 0) {
     auto result = f->write_unlocked(str_view.data(), str_view.size());
@@ -36,21 +24,12 @@ int write_out(cpp::string_view str_view, File *f) {
   return 0;
 }
 
-} // namespace
-
-// TODO: this seems like there should be some sort of queue system to
-// deduplicate this code.
-LLVM_LIBC_FUNCTION(void, perror, (const char *str)) {
-  const char empty_str[1] = {'\0'};
-  if (str == nullptr)
-    str = empty_str;
-  cpp::string_view str_view(str);
-
-  auto err_str = get_error_string(libc_errno);
-
-  // We need to lock the stream to ensure the newline is always appended.
-  ScopedLock lock(LIBC_NAMESPACE::stderr);
+// separate function so that we can return early on error but still get the
+// unlock. This function sets errno and should not be called elsewhere.
+void write_sequence(cpp::string_view str_view, cpp::string_view err_str) {
   int write_err;
+  // TODO: this seems like there should be some sort of queue system to
+  // deduplicate this code.
 
   // FORMAT:
   // if str != nullptr and doesn't start with a null byte:
@@ -84,4 +63,18 @@ LLVM_LIBC_FUNCTION(void, perror, (const char *str)) {
   }
 }
 
+LLVM_LIBC_FUNCTION(void, perror, (const char *str)) {
+  const char empty_str[1] = {'\0'};
+  if (str == nullptr)
+    str = empty_str;
+  cpp::string_view str_view(str);
+
+  cpp::string_view err_str = get_error_string(libc_errno);
+
+  // We need to lock the stream to ensure the newline is always appended.
+  LIBC_NAMESPACE::stderr->lock();
+  write_sequence(str_view, err_str);
+  LIBC_NAMESPACE::stderr->unlock();
+}
+
 } // namespace LIBC_NAMESPACE_DECL

>From ddb29a88f1cf5f3266ee71bc65d6cf3866bedec5 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Wed, 11 Jun 2025 13:15:39 -0700
Subject: [PATCH 3/3] mark static and add to riscv and aarch64

---
 libc/config/linux/aarch64/entrypoints.txt | 1 +
 libc/config/linux/riscv/entrypoints.txt   | 1 +
 libc/src/stdio/generic/perror.cpp         | 5 +++--
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index 520046f768b5d..16131438e6ca9 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -969,6 +969,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.perror
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index 0b645a2d2fb8b..cc939214c82bf 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1095,6 +1095,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.perror
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/src/stdio/generic/perror.cpp b/libc/src/stdio/generic/perror.cpp
index 2c39df55f8adc..897183eafcaf6 100644
--- a/libc/src/stdio/generic/perror.cpp
+++ b/libc/src/stdio/generic/perror.cpp
@@ -15,7 +15,7 @@
 
 namespace LIBC_NAMESPACE_DECL {
 
-int write_out(cpp::string_view str_view, File *f) {
+static int write_out(cpp::string_view str_view, File *f) {
   if (str_view.size() > 0) {
     auto result = f->write_unlocked(str_view.data(), str_view.size());
     if (result.has_error())
@@ -26,7 +26,8 @@ int write_out(cpp::string_view str_view, File *f) {
 
 // separate function so that we can return early on error but still get the
 // unlock. This function sets errno and should not be called elsewhere.
-void write_sequence(cpp::string_view str_view, cpp::string_view err_str) {
+static void write_sequence(cpp::string_view str_view,
+                           cpp::string_view err_str) {
   int write_err;
   // TODO: this seems like there should be some sort of queue system to
   // deduplicate this code.



More information about the libc-commits mailing list