From aad7cf00bda1209980f2f611f2d60a9cf53db94d Mon Sep 17 00:00:00 2001
From: Martin Marinov <Martin.Marinov@autodesk.com>
Date: Wed, 6 Apr 2022 21:06:29 +0100
Subject: [PATCH] MTBR-925 Add BufferedOutputFile (#79)

Add buffered text output file for faster text writes. Improve platform compatibility for printing.
---
 Utils/BufferedOutputFile.hh | 102 ++++++++++++++++++++++++++++++++++++
 Utils/CMakeLists.txt        |   1 +
 Utils/OStringStream.cc      |  24 +++++----
 3 files changed, 116 insertions(+), 11 deletions(-)
 create mode 100644 Utils/BufferedOutputFile.hh

diff --git a/Utils/BufferedOutputFile.hh b/Utils/BufferedOutputFile.hh
new file mode 100644
index 0000000..61222b1
--- /dev/null
+++ b/Utils/BufferedOutputFile.hh
@@ -0,0 +1,102 @@
+// (C) Copyright 2022 by Autodesk, Inc.
+
+#ifndef BASE_BUFFEREDOUTPUTFILE_HH_INCLUDED
+#define BASE_BUFFEREDOUTPUTFILE_HH_INCLUDED
+
+#include <Base/Config/BaseDefines.hh>
+#include <Base/Utils/IOutputStream.hh>
+#include <Base/Debug/DebOut.hh>
+
+#include <cstdio>
+#include <vector>
+
+namespace Base
+{
+
+// Provide buffered text print operations to a FILE, helps with the performance
+// when storing large text data.
+class BufferedOutputFile
+{
+public:
+  BufferedOutputFile(const char* const _flnm, // filename
+      const size_t _flsh_size = 1 << 20, // buffer size that forces flush (1Mb)
+      const size_t _ovfl_size = 1 << 10 // "overflow" space in the buffer, also
+      // sets the maximum amount of characters that can be printed at once (1Kb)
+      )
+      : flsh_size_(_flsh_size), ovfl_size_(_ovfl_size),
+        bffr_(flsh_size_ + ovfl_size_), bffr_end_(bffr_.data())
+  {
+    const char* const MODE = "wb"; // open as write binary
+
+#ifdef _MSC_VER
+        fopen_s(&file_, _flnm, MODE);
+#else
+        file_ = std::fopen(_flnm, MODE);
+#endif
+  }
+
+  ~BufferedOutputFile()
+  {
+    if (!opened())
+      return;
+    flush();
+    fclose(file_);
+  }
+
+  bool opened() const { return file_ != nullptr; }
+
+#define DEB_check_opened \
+  DEB_error_if(!opened(), "Write access to a file that has failed open")
+
+  void flush()
+  {
+    DEB_check_opened;
+    const auto bffr_size = buffer_size();
+    if (bffr_size == 0)
+      return;
+    fwrite(bffr_.data(), sizeof(char), bffr_size, file_);
+    bffr_end_ = bffr_.data();
+  }
+
+  template <typename... ArgT> void print(const ArgT&... _args)
+  {
+    DEB_check_opened;
+    const auto char_nmbr = Base::print(bffr_end_, ovfl_size_, _args...);
+    DEB_error_if(char_nmbr < 0 || char_nmbr >= static_cast<int>(ovfl_size_),
+        "print() result = "
+            << char_nmbr << " is unclear, consider using larger overflow size");
+    bffr_end_ += char_nmbr;
+    if (buffer_size() + ovfl_size_ >= bffr_.size()) // make sure we always have
+      flush(); // at least ovfl_size_ space in the buffer
+  }
+
+  template <typename T> void write(const T* const _ptr, const size_t _nmbr)
+  {
+    DEB_check_opened;
+    flush();
+    fwrite(_ptr, sizeof(T), _nmbr, file_);
+  }
+
+  // Access the file, but make sure we have flushed first
+  FILE* file()
+  {
+    DEB_check_opened;
+    flush();
+    return file_;
+  }
+
+#undef DEB_check_opened
+
+private:
+  const size_t flsh_size_; // number of bytes to trigger flushing the buffer
+  const size_t ovfl_size_; // number of bytes to print in a single print() call
+  std::vector<char> bffr_; // buffer
+  char* bffr_end_;         // end of the buffer
+  FILE* file_ = nullptr;
+
+  size_t buffer_size() const { return bffr_end_ - bffr_.data(); }
+};
+
+} // namespace Base
+
+#endif // BASE_BUFFEREDOUTPUTFILE_HH_INCLUDED
diff --git a/Utils/CMakeLists.txt b/Utils/CMakeLists.txt
index 7fc0247..80ca9e9 100644
--- a/Utils/CMakeLists.txt
+++ b/Utils/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(my_headers
    ${CMAKE_CURRENT_SOURCE_DIR}/BaseError.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/BaseErrorInc.hh
+   ${CMAKE_CURRENT_SOURCE_DIR}/BufferedOutputFile.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/Environment.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/FileOutput.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/IOutputStream.hh
diff --git a/Utils/OStringStream.cc b/Utils/OStringStream.cc
index 5eba038..f1cf110 100644
--- a/Utils/OStringStream.cc
+++ b/Utils/OStringStream.cc
@@ -1,4 +1,4 @@
-// (C) Copyright 2019 by Autodesk, Inc.
+// (C) Copyright 2022 by Autodesk, Inc.
 
 #include "Base/Security/Mandatory.hh"
 #include "OStringStream.hh"
@@ -26,16 +26,22 @@ namespace Base
 namespace
 {
 
+// Cross-platform approach to avoid buffer overflow when printing. However, 
+// there are subtle differences between these functions, see here for details:
+// https://stackoverflow.com/questions/46485639/what-is-the-difference-between-vsnprintf-and-vsprintf-s
+
+#ifdef _MSC_VER
+  #define VSNPRINTF vsprintf_s
+#else
+  #define VSNPRINTF vsnprintf
+#endif
+
 template <size_t _bffr_size>
 int sprintf_s(char (&_bffr)[_bffr_size], const char* _frmt, ...)
 {
   va_list arg_ptr;
   va_start(arg_ptr, _frmt);
-#ifdef _MSC_VER
-  int res = vsprintf_s(_bffr, _bffr_size, _frmt, arg_ptr);
-#else
-  int res = vsprintf(_bffr, _frmt, arg_ptr);
-#endif // _MSC_VER
+  int res = VSNPRINTF(_bffr, _bffr_size, _frmt, arg_ptr);
   va_end(arg_ptr);
   return res;
 }
@@ -46,11 +52,7 @@ int print(char* _bffr, const size_t _bffr_size, const char* _frmt, ...)
 {
   va_list arg_ptr;
   va_start(arg_ptr, _frmt);
-#ifdef _MSC_VER
-  int res = vsprintf_s(_bffr, _bffr_size, _frmt, arg_ptr);
-#else
-  int res = vsprintf(_bffr, _frmt, arg_ptr);
-#endif
+  int res = VSNPRINTF(_bffr, _bffr_size, _frmt, arg_ptr);
   va_end(arg_ptr);
   return res;
 }
-- 
GitLab