print: Add c++23 print compatible implementation

Change-Id: I2bb81f79550f3e9bdb0ea15cb21225a015d17800
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/include/meson.build b/include/meson.build
index d24e818..57a5a5e 100644
--- a/include/meson.build
+++ b/include/meson.build
@@ -17,6 +17,7 @@
   'stdplus/numeric/endian.hpp',
   'stdplus/numeric/str.hpp',
   'stdplus/pinned.hpp',
+  'stdplus/print.hpp',
   'stdplus/raw.hpp',
   'stdplus/signal.hpp',
   'stdplus/str/buf.hpp',
diff --git a/include/stdplus/print.hpp b/include/stdplus/print.hpp
new file mode 100644
index 0000000..d74f2f1
--- /dev/null
+++ b/include/stdplus/print.hpp
@@ -0,0 +1,57 @@
+#include <stdplus/str/buf.hpp>
+
+#include <cstdio>
+#include <format>
+#include <system_error>
+
+namespace stdplus
+{
+
+template <bool ln>
+struct Printer
+{
+    template <typename... Args>
+    static void print(std::FILE* stream, std::format_string<Args...> fmt,
+                      Args&&... args)
+    {
+        stdplus::StrBuf buf;
+        std::format_to(std::back_inserter(buf), fmt,
+                       std::forward<Args>(args)...);
+        if constexpr (ln)
+        {
+            buf.push_back('\n');
+        }
+        int r = std::fwrite(buf.data(), sizeof(char), buf.size(), stream);
+        if (r < 0)
+        {
+            throw std::system_error(errno, std::generic_category());
+        }
+    }
+};
+
+template <typename... Args>
+void print(std::FILE* stream, std::format_string<Args...> fmt, Args&&... args)
+{
+    Printer<false>::print(stream, fmt, std::forward<Args>(args)...);
+}
+
+template <typename... Args>
+inline void print(std::format_string<Args...> fmt, Args&&... args)
+{
+    Printer<false>::print(stdout, fmt, std::forward<Args>(args)...);
+}
+
+template <typename... Args>
+inline void println(std::FILE* stream, std::format_string<Args...> fmt,
+                    Args&&... args)
+{
+    Printer<true>::print(stream, fmt, std::forward<Args>(args)...);
+}
+
+template <typename... Args>
+inline void println(std::format_string<Args...> fmt, Args&&... args)
+{
+    Printer<true>::print(stdout, fmt, std::forward<Args>(args)...);
+}
+
+} // namespace stdplus
diff --git a/src/meson.build b/src/meson.build
index 0714b56..11549a3 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -57,6 +57,7 @@
   'numeric/endian.cpp',
   'numeric/str.cpp',
   'pinned.cpp',
+  'print.cpp',
   'raw.cpp',
   'signal.cpp',
   'str/buf.cpp',
diff --git a/src/print.cpp b/src/print.cpp
new file mode 100644
index 0000000..4c16fe3
--- /dev/null
+++ b/src/print.cpp
@@ -0,0 +1 @@
+#include <stdplus/print.hpp>
diff --git a/test/meson.build b/test/meson.build
index 40ac624..a0c37a1 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -13,6 +13,7 @@
   'numeric/endian': [stdplus_dep, gtest_main_dep],
   'numeric/str': [stdplus_dep, gtest_main_dep],
   'pinned': [stdplus_dep, gtest_main_dep],
+  'print': [stdplus_dep, gtest_main_dep],
   'raw': [stdplus_dep, gmock_dep, gtest_main_dep],
   'signal': [stdplus_dep, gtest_main_dep],
   'str/buf': [stdplus_dep, gtest_main_dep],
diff --git a/test/print.cpp b/test/print.cpp
new file mode 100644
index 0000000..4e511ed
--- /dev/null
+++ b/test/print.cpp
@@ -0,0 +1,31 @@
+#include <stdplus/print.hpp>
+
+#include <cstdio>
+
+#include <gtest/gtest.h>
+
+namespace stdplus
+{
+
+TEST(Print, Basic)
+{
+    auto file = std::tmpfile();
+    print(file, "hello");
+    print(file, "hi {}\n", 4);
+    println(file, "ho\n");
+    println(file, "ho {}", 16);
+    EXPECT_EQ(0, std::fseek(file, 0, SEEK_SET));
+    constexpr std::string_view expect = "hellohi 4\nho\n\nho 16\n";
+    std::string buf(expect.size(), '\0');
+    EXPECT_EQ(buf.size(),
+              std::fread(buf.data(), sizeof(char), buf.size() + 1, file));
+    EXPECT_EQ(buf, expect);
+    EXPECT_EQ(0, std::fclose(file));
+
+    print("hello");
+    print("hi {}\n", 4);
+    println("ho\n");
+    println("ho {}", 16);
+}
+
+} // namespace stdplus