[sw] Move printf-like facilities into libbase.

|printf|-like functions simply move bytes around in a fancy way, and
having them accessible everywhere is probably useful.

As #1162 is addressed, we will see if we can use some link-time
mechanism to more effectively control the "default" sink used by
|base_printf()|.

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/sw/device/lib/base/meson.build b/sw/device/lib/base/meson.build
index 7776626..a69ebcf 100644
--- a/sw/device/lib/base/meson.build
+++ b/sw/device/lib/base/meson.build
@@ -20,3 +20,21 @@
     sources: ['mmio.c'],
   )
 )
+
+# Basic printing library (sw_lib_base_print)
+sw_lib_base_print = declare_dependency(
+  link_with: static_library(
+    'base_print_ot',
+    sources: ['print.c'],
+  )
+)
+
+test('sw_lib_base_print_test', executable(
+  'sw_lib_base_print_test',
+  sources: ['print.c', 'print_test.cc'],
+  dependencies: [
+    sw_vendor_gtest,
+  ],
+  native: true,
+))
+
diff --git a/sw/device/lib/base/print.c b/sw/device/lib/base/print.c
new file mode 100644
index 0000000..5390003
--- /dev/null
+++ b/sw/device/lib/base/print.c
@@ -0,0 +1,345 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/base/print.h"
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+// This is declared as an enum to force the values to be
+// compile-time constants, but the type is otherwise not
+// used for anything.
+enum {
+  // Standard format specifiers.
+  kPercent = '%',
+  kCharacter = 'c',
+  kString = 's',
+  kSignedDec1 = 'd',
+  kSignedDec2 = 'i',
+  kUnsignedOct = 'o',
+  kUnsignedHexLow = 'x',
+  kUnsignedHexHigh = 'X',
+  kUnsignedDec = 'u',
+  kPointer = 'p',
+
+  // Verilog-style format specifiers.
+  kSvBinary = 'b',
+  kSvHexLow = 'h',
+  kSvHexHigh = 'H',
+
+  // Other non-standard specifiers.
+  kSizedStr = 'z',
+};
+
+// NOTE: all of the lengths of the strings below are given so that the NUL
+// terminator is left off; that way, |sizeof(kConst)| does not include it.
+static const char kDigitsLow[16] = "0123456789abcdef";
+static const char kDigitsHigh[16] = "0123456789ABCDEF";
+
+static const char kErrorNul[17] = "%<unexpected nul>";
+static const char kUnknownSpec[15] = "%<unknown spec>";
+static const char kErrorTooWide[12] = "%<bad width>";
+
+static size_t base_dev_null(void *data, const char *buf, size_t len) {
+  return len;
+}
+static buffer_sink_t base_stdout = {
+    .data = NULL,
+    .sink = &base_dev_null,
+};
+
+void base_set_stdout(buffer_sink_t out) {
+  if (out.sink == NULL) {
+    out.sink = &base_dev_null;
+  }
+  base_stdout = out;
+}
+
+size_t base_printf(const char *format, ...) {
+  va_list args;
+  va_start(args, format);
+  size_t bytes_left = base_vfprintf(base_stdout, format, args);
+  va_end(args);
+  return bytes_left;
+}
+
+typedef struct snprintf_captures_t {
+  char *buf;
+  size_t bytes_left;
+} snprintf_captures_t;
+
+static size_t snprintf_sink(void *data, const char *buf, size_t len) {
+  snprintf_captures_t *captures = (snprintf_captures_t *)data;
+  if (captures->bytes_left == 0) {
+    return 0;
+  }
+
+  if (len > captures->bytes_left) {
+    len = captures->bytes_left;
+  }
+  __builtin_memcpy(captures->buf, buf, len);
+  captures->buf += len;
+  captures->bytes_left -= len;
+  return len;
+}
+
+size_t base_snprintf(char *buf, size_t len, const char *format, ...) {
+  va_list args;
+  va_start(args, format);
+
+  snprintf_captures_t data = {
+      .buf = buf,
+      .bytes_left = len,
+  };
+  buffer_sink_t out = {
+      .data = &data,
+      .sink = &snprintf_sink,
+  };
+  size_t bytes_left = base_vfprintf(out, format, args);
+  va_end(args);
+  return bytes_left;
+}
+
+size_t base_fprintf(buffer_sink_t out, const char *format, ...) {
+  va_list args;
+  va_start(args, format);
+  size_t bytes_left = base_vfprintf(out, format, args);
+  va_end(args);
+  return bytes_left;
+}
+
+/**
+ * Consumes characters from |format| until a '%' or NUL is reached. All
+ * characters seen before that are then sinked into |out|.
+ *
+ * @param out the sink to write bytes to.
+ * @param format a pointer to the format string to consume a prefix of.
+ * @param bytes_written out param for the number of bytes writen to |out|.
+ * @return true if an unprocessed '%' was found.
+ */
+static bool consume_until_percent(buffer_sink_t out, const char **format,
+                                  size_t *bytes_written) {
+  size_t text_len = 0;
+  while (true) {
+    char c = (*format)[text_len];
+    if (c == '\0' || c == kPercent) {
+      if (text_len > 0) {
+        *bytes_written += out.sink(out.data, *format, text_len);
+      }
+      *format += text_len;
+      return c != '\0';
+    }
+    ++text_len;
+  }
+}
+
+/**
+ * Represents a parsed format specifier.
+ */
+typedef struct format_specifier {
+  char type;
+  size_t width;
+} format_specifier_t;
+
+/**
+ * Consumes characters from |format| until a complete format specifier is
+ * parsed. See the documentation in |print.h| for full syntax.
+ *
+ * @param out the sink to write bytes to.
+ * @param format a pointer to the format string to consume a prefix of.
+ * @param spec out param for the specifier.
+ * @return whether the parse succeeded.
+ */
+static bool consume_format_specifier(buffer_sink_t out, const char **format,
+                                   size_t *bytes_written,
+                                   format_specifier_t *spec) {
+  *spec = (format_specifier_t){0};
+
+  // Consume the percent sign.
+  ++(*format);
+
+  // Attempt to parse out an unsigned, decimal number, a "width",
+  // after the percent sign; the format specifier is the character
+  // immediately after this width.
+  size_t spec_len = 0;
+  bool has_width = false;
+  while (true) {
+    char c = (*format)[spec_len];
+    if (c == '\0') {
+      *bytes_written += out.sink(out.data, kErrorNul, sizeof(kErrorNul));
+      return false;
+    }
+    if (c < '0' || c > '9') {
+      break;
+    }
+    has_width = true;
+    spec->width *= 10;
+    spec->width += (c - '0');
+    ++spec_len;
+  }
+
+  if ((spec->width == 0 && has_width) || spec->width > 32) {
+    *bytes_written += out.sink(out.data, kErrorTooWide, sizeof(kErrorTooWide));
+    return false;
+  }
+
+  spec->type = (*format)[spec_len];
+  *format += spec_len + 1;
+  return true;
+}
+
+/**
+ * Write the digits of |value| onto |out|.
+ *
+ * @param out the sink to write bytes to.
+ * @param value the value to "stringify".
+ * @param width the minimum width to print; going below will result in writing
+ *        out zeroes.
+ * @param base the base to express |value| in.
+ * @param glyphs an array of characters to use as the digits of a number, which
+ *        should be at least ast long as |base|.
+ * @return the number of bytes written.
+ */
+static size_t write_digits(buffer_sink_t out, uint32_t value, uint32_t width,
+                           uint32_t base, const char *glyphs) {
+  // All allocations are done relative to a buffer that could hold the longest
+  // textual representation of a number: ~0x0 in base 2, i.e., 32 ones.
+  static const int kWordBits = sizeof(uint32_t) * 8;
+  char buffer[kWordBits];
+
+  size_t len = 0;
+  while (value > 0) {
+    uint32_t digit = value % base;
+    value /= base;
+    buffer[kWordBits - 1 - len] = glyphs[digit];
+    ++len;
+  }
+  width = width == 0 ? 1 : width;
+  width = width > kWordBits ? kWordBits : width;
+  while (len < width) {
+    buffer[kWordBits - len - 1] = '0';
+    ++len;
+  }
+  return out.sink(out.data, buffer + (kWordBits - len), len);
+}
+
+/**
+ * Prints out the next entry in |args| according to |spec|.
+ *
+ * This function assumes that |spec| accurately describes the next entry in
+ * |args|.
+ *
+ * @param out the sink to write bytes to.
+ * @param spec the specifier to use for stringifying.
+ * @param bytes_written out param for the number of bytes writen to |out|.
+ * @param va_list the list to pull an entry from.
+ */
+static void process_specifier(buffer_sink_t out, format_specifier_t spec,
+                              size_t *bytes_written, va_list args) {
+  // Switch on the specifier. At this point, we assert that there is
+  // an initialized value of correct type in the VA list; if it is
+  // missing, the caller has caused UB.
+  switch (spec.type) {
+    case kPercent: {
+      *bytes_written += out.sink(out.data, "%", 1);
+      break;
+    }
+    case kCharacter: {
+      char value = (char)va_arg(args, uint32_t);
+      *bytes_written += out.sink(out.data, &value, 1);
+      break;
+    }
+    case kString: {
+      char *value = va_arg(args, char *);
+      size_t len = 0;
+      while (value[len] != '\0') {
+        ++len;
+      }
+      *bytes_written += out.sink(out.data, value, len);
+      break;
+    }
+    case kSizedStr: {
+      size_t len = va_arg(args, size_t);
+      char *value = va_arg(args, char *);
+      *bytes_written += out.sink(out.data, value, len);
+      break;
+    }
+    case kSignedDec1:
+    case kSignedDec2: {
+      uint32_t value = va_arg(args, uint32_t);
+      if (((int32_t)value) < 0) {
+        *bytes_written += out.sink(out.data, "-", 1);
+        value = -value;
+      }
+      *bytes_written += write_digits(out, value, spec.width, 10, kDigitsLow);
+      break;
+    }
+    case kUnsignedOct: {
+      uint32_t value = va_arg(args, uint32_t);
+      *bytes_written += write_digits(out, value, spec.width, 8, kDigitsLow);
+      break;
+    }
+    case kPointer: {
+      // Pointers are formatted as 0x<hex digits>, where the width is always
+      // set to the number necessary to represent a pointer on the current
+      // platform, that is, the size of uintptr_t in nybbles. For example, on
+      // different architecutres the null pointer prints as
+      // - rv32imc: 0x00000000 (four bytes, eight nybbles).
+      // - amd64:   0x0000000000000000 (eight bytes, sixteen nybbles).
+      *bytes_written += out.sink(out.data, "0x", 2);
+      uintptr_t value = va_arg(args, uintptr_t);
+      *bytes_written += write_digits(out, value, sizeof(uintptr_t) * 2, 16, kDigitsLow);
+      break;
+    }
+    case kSvHexLow:
+    case kUnsignedHexLow: {
+      uint32_t value = va_arg(args, uint32_t);
+      *bytes_written += write_digits(out, value, spec.width, 16, kDigitsLow);
+      break;
+    }
+    case kSvHexHigh:
+    case kUnsignedHexHigh: {
+      uint32_t value = va_arg(args, uint32_t);
+      *bytes_written += write_digits(out, value, spec.width, 16, kDigitsHigh);
+      break;
+    }
+    case kUnsignedDec: {
+      uint32_t value = va_arg(args, uint32_t);
+      *bytes_written += write_digits(out, value, spec.width, 10, kDigitsLow);
+      break;
+    }
+    case kSvBinary: {
+      uint32_t value = va_arg(args, uint32_t);
+      *bytes_written += write_digits(out, value, spec.width, 2, kDigitsLow);
+      break;
+    }
+    default: {
+      *bytes_written += out.sink(out.data, kUnknownSpec, sizeof(kUnknownSpec));
+    }
+  }
+}
+
+size_t base_vfprintf(buffer_sink_t out, const char *format, va_list args) {
+  if (out.sink == NULL) {
+    out.sink = &base_dev_null;
+  }
+
+  size_t bytes_written = 0;
+  while (format[0] != '\0') {
+    if (!consume_until_percent(out, &format, &bytes_written)) {
+      break;
+    }
+
+    format_specifier_t spec;
+    if (!consume_format_specifier(out, &format, &bytes_written, &spec)) {
+      break;
+    }
+
+    process_specifier(out, spec, &bytes_written, args);
+  }
+
+  return bytes_written;
+}
diff --git a/sw/device/lib/base/print.h b/sw/device/lib/base/print.h
new file mode 100644
index 0000000..7b81cd2
--- /dev/null
+++ b/sw/device/lib/base/print.h
@@ -0,0 +1,151 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_BASE_PRINT_H_
+#define OPENTITAN_SW_DEVICE_LIB_BASE_PRINT_H_
+
+#include <stdarg.h>
+#include <stddef.h>
+
+/**
+ * This header provides libc-like printing facilities, which is agnostic of the
+ * underlying hardware printing mechanism.
+ *
+ * We avoid using libc names here, since we do not support the full suite of
+ * format specifier syntax, and use a different character sink type instead of
+ * the traditional |FILE *|.
+ *
+ * All functions in this file should be machine word size agnostic, that is, the
+ * same code should work correctly on both 32-bit and 64-bit machines, though
+ * formatting, where the exact format style is unspecified, is allowed to vary
+ * slightly on machine word size.
+ */
+
+/**
+ * A buffer_sink_t represents a place to write bytes to, implemented as a
+ * C-style "closure".
+ *
+ * It consists of a generic data pointer, which can hold instance-specific
+ * information, and a sink function, which takes the data pointer, a buffer, and
+ * that buffer's length.
+ *
+ * The sink function should return the number of bytes actually written.
+ */
+typedef struct buffer_sink {
+  void *data;
+  size_t (*sink)(void *data, const char *buf, size_t len);
+} buffer_sink_t;
+
+/**
+ * Prints out a message to stdout, formatted according to the format string
+ * |format|.
+ *
+ * The definition of "stdout" is not provided by this library; rather, it must
+ * be initialized using |base_set_stdout()|.
+ *
+ * This function supports a subset of the format specifiers provided by standard
+ * C |printf|. Those are, namely:
+ * - %%, which prints a percent sign.
+ * - %c, which prints the lowest byte of a uint32_t as a character.
+ * - %s, which prints a NUL-terminated string.
+ * - %d and %i, which print a signed decimal uint32_t.
+ * - %u, which prints an unsigned decimal uint32_t.
+ * - %o, which prints an unsigned octal uint32_t.
+ * - %x and %X, which print an unsigned hex uint32_t.
+ * - %p, which prints a pointer in a consistent but unspecified way.
+ *
+ * Additionally, three SystemVerilog format specifiers are supported:
+ * - %h and %H, which are aliases for %x and %X, respectively.
+ * - %b, which prints an unsigned binary uint32_t.
+ *
+ * Finally, an additional nonstandard format specifier is supported:
+ * - %z, which takes a size_t followed by a pointer to a buffer, and prints
+ *   out that many characters from the buffer.
+ *
+ * When compiled for a DV testbench, this function will not read any pointers,
+ * and as such the specifiers %s and %z will behave as if they were printing
+ * garbage, and are, as such, unsupported.
+ *
+ * This function furthermore supports width modifiers for integer specifiers,
+ * such as |%10d|. It does not support dynamic widths like |%*d|, and will also
+ * always pad with zeroes, rather than spaces.
+ *
+ * Of course, providing arguments for formatting which are incompatible with a
+ * given format specifier is Undefined Behavior.
+ *
+ * @param format the format spec.
+ * @param ... values to interpolate in the format spec.
+ */
+size_t base_printf(const char *format, ...);
+
+/*
+ * Prints a message to the buffer |buf|, capped at a given length.
+ *
+ * It goes without saying that the caller must ensure the given buffer is large
+ * enough; failure to do so is Undefined Behavior.
+ *
+ * See |base_printf()| for the semantics of the format specification.
+ *
+ * @param buf a buffer to print to.
+ * @param format the format spec.
+ * @param ... values to interpolate in the format spec.
+ */
+size_t base_snprintf(char *buf, size_t len, const char *format, ...);
+
+/**
+ * Prints a message to the sink |out|.
+ *
+ * If |out.sink| is |NULL|, writes are treated as-if they were written to a
+ * UNIX-like /dev/null: writes succeed, but the actual bytes are not printed
+ * anywhere.
+ *
+ * See |base_printf()| for the semantics of the format specification.
+ *
+ * @param out a sink to print to.
+ * @param format the format spec.
+ * @param ... values to interpolate in the format spec.
+ */
+size_t base_fprintf(buffer_sink_t out, const char *format, ...);
+
+/**
+ * Prints a message to the sink |out|.
+ *
+ * This function is identical to |base_fprintf|, except in that it takes a
+ * |va_list| instead of having a vararg parameter. This function is provided
+ * not for calling directly, but rather for being called by functions that
+ * already take a variable number of arguments, and wish to make use of
+ * formatting facilities.
+ *
+ * This function *does not* take ownership of |args|; callers are responsible
+ * for calling |va_end|.
+ *
+ * If |out.sink| is |NULL|, writes are treated as-if they were written to a
+ * UNIX-like /dev/null: writes succeed, but the actual bytes are not printed
+ * anywhere.
+ *
+ * See |base_printf()| for the semantics of the format specification.
+ *
+ * @param out a sink to print to.
+ * @param format the format spec.
+ * @param args values to interpolate in the format spec.
+ */
+size_t base_vfprintf(buffer_sink_t out, const char *format, va_list args);
+
+/**
+ * Sets what the "stdout" sink is, which is used by |base_printf()|.
+ *
+ * The default sink behaves like /dev/null on a standard UNIX system: writes
+ * are treated as successful, but the contents of buffers are ignored.
+ *
+ * As such, this function must be called for printed messages to wind up
+ * somewhere.
+ *
+ * Passing in |NULL| instead of a real function pointer will reset stdout to
+ * the default /dev/null behavior.
+ *
+ * @param out the sink to use for "default" printing.
+ */
+void base_set_stdout(buffer_sink_t out);
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_BASE_PRINT_H_
diff --git a/sw/device/lib/base/print_test.cc b/sw/device/lib/base/print_test.cc
new file mode 100644
index 0000000..3399735
--- /dev/null
+++ b/sw/device/lib/base/print_test.cc
@@ -0,0 +1,241 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+extern "C" {
+#include "sw/device/lib/base/print.h"
+}  // extern "C"
+
+#include <stdint.h>
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace base {
+namespace {
+
+using ::testing::StartsWith;
+
+// A test fixture for automatiocally capturing stdout.
+class PrintfTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    base_set_stdout({/*data=*/static_cast<void *>(&buf_),
+                     /*sink=*/+[](void *data, const char *buf, size_t len) {
+                       static_cast<std::string *>(data)->append(buf, len);
+                       return len;
+                     }});
+  }
+
+  std::string buf_;
+};
+
+TEST_F(PrintfTest, EmptyFormat) {
+  EXPECT_EQ(base_printf(""), 0);
+  EXPECT_EQ(buf_, "");
+}
+
+TEST_F(PrintfTest, TrivialText) {
+  EXPECT_EQ(base_printf("Hello, World!\n"), 14);
+  EXPECT_EQ(buf_, "Hello, World!\n");
+}
+
+TEST_F(PrintfTest, PartialPrints) {
+  EXPECT_EQ(base_printf("Hello, "), 7);
+  EXPECT_EQ(buf_, "Hello, ");
+  EXPECT_EQ(base_printf("World!\n"), 7);
+  EXPECT_EQ(buf_, "Hello, World!\n");
+}
+
+TEST_F(PrintfTest, LiteralPct) {
+  EXPECT_EQ(base_printf("Hello, %%!\n"), 10);
+  EXPECT_EQ(buf_, "Hello, %!\n");
+}
+
+TEST_F(PrintfTest, Character) {
+  EXPECT_EQ(base_printf("Hello, %c!\n", 'X'), 10);
+  EXPECT_EQ(buf_, "Hello, X!\n");
+}
+
+TEST_F(PrintfTest, StringWithNul) {
+  EXPECT_EQ(base_printf("Hello, %s!\n", "abcxyz"), 15);
+  EXPECT_EQ(buf_, "Hello, abcxyz!\n");
+}
+
+TEST_F(PrintfTest, StringWithLen) {
+  EXPECT_EQ(base_printf("Hello, %z!\n", 6, "abcxyz"), 15);
+  EXPECT_EQ(buf_, "Hello, abcxyz!\n");
+}
+
+TEST_F(PrintfTest, StringWithLenPrefix) {
+  EXPECT_EQ(base_printf("Hello, %z!\n", 3, "abcxyz"), 12);
+  EXPECT_EQ(buf_, "Hello, abc!\n");
+}
+
+TEST_F(PrintfTest, StringWithLenZeroLen) {
+  EXPECT_EQ(base_printf("Hello, %z!\n", 0, "abcxyz"), 9);
+  EXPECT_EQ(buf_, "Hello, !\n");
+}
+
+TEST_F(PrintfTest, SignedInt) {
+  EXPECT_EQ(base_printf("Hello, %i!\n", 42), 11);
+  EXPECT_EQ(buf_, "Hello, 42!\n");
+}
+
+TEST_F(PrintfTest, SignedIntAlt) {
+  EXPECT_EQ(base_printf("Hello, %d!\n", 42), 11);
+  EXPECT_EQ(buf_, "Hello, 42!\n");
+}
+
+TEST_F(PrintfTest, SignedIntNegative) {
+  EXPECT_EQ(base_printf("Hello, %i!\n", -800), 13);
+  EXPECT_EQ(buf_, "Hello, -800!\n");
+}
+
+TEST_F(PrintfTest, SignedIntWithWidth) {
+  EXPECT_EQ(base_printf("Hello, %3i!\n", 42), 12);
+  EXPECT_EQ(buf_, "Hello, 042!\n");
+}
+
+TEST_F(PrintfTest, SignedIntWithWidthTooShort) {
+  EXPECT_EQ(base_printf("Hello, %3i!\n", 9001), 13);
+  EXPECT_EQ(buf_, "Hello, 9001!\n");
+}
+
+TEST_F(PrintfTest, UnsignedInt) {
+  EXPECT_EQ(base_printf("Hello, %u!\n", 42), 11);
+  EXPECT_EQ(buf_, "Hello, 42!\n");
+}
+
+TEST_F(PrintfTest, UnsignedIntNegative) {
+  EXPECT_EQ(base_printf("Hello, %u!\n", -1), 19);
+  EXPECT_EQ(buf_, "Hello, 4294967295!\n");
+}
+
+TEST_F(PrintfTest, HexFromDec) {
+  EXPECT_EQ(base_printf("Hello, %x!\n", 1024), 12);
+  EXPECT_EQ(buf_, "Hello, 400!\n");
+}
+
+TEST_F(PrintfTest, HexFromDecWithWidth) {
+  EXPECT_EQ(base_printf("Hello, %8x!\n", 1024), 17);
+  EXPECT_EQ(buf_, "Hello, 00000400!\n");
+}
+
+TEST_F(PrintfTest, HexLower) {
+  EXPECT_EQ(base_printf("Hello, %x!\n", 0xdead'beef), 17);
+  EXPECT_EQ(buf_, "Hello, deadbeef!\n");
+}
+
+TEST_F(PrintfTest, HexUpper) {
+  EXPECT_EQ(base_printf("Hello, %X!\n", 0xdead'beef), 17);
+  EXPECT_EQ(buf_, "Hello, DEADBEEF!\n");
+}
+
+TEST_F(PrintfTest, HexNegative) {
+  EXPECT_EQ(base_printf("Hello, %x!\n", -1), 17);
+  EXPECT_EQ(buf_, "Hello, ffffffff!\n");
+}
+
+TEST_F(PrintfTest, HexSvLower) {
+  EXPECT_EQ(base_printf("Hello, %h!\n", 0xdead'beef), 17);
+  EXPECT_EQ(buf_, "Hello, deadbeef!\n");
+}
+
+TEST_F(PrintfTest, HexSvUpper) {
+  EXPECT_EQ(base_printf("Hello, %H!\n", 0xdead'beef), 17);
+  EXPECT_EQ(buf_, "Hello, DEADBEEF!\n");
+}
+
+TEST_F(PrintfTest, Pointer) {
+  auto *ptr = reinterpret_cast<uint32_t *>(0x1234);
+  base_printf("Hello, %p!\n", ptr);
+  switch (sizeof(uintptr_t)) {
+    case 4: 
+      EXPECT_EQ(buf_, "Hello, 0x00001234!\n");
+      break;
+    case 8:
+      EXPECT_EQ(buf_, "Hello, 0x0000000000001234!\n");
+      break;
+  }
+}
+
+TEST_F(PrintfTest, NullPtr) {
+  base_printf("Hello, %p!\n", nullptr);
+  switch (sizeof(uintptr_t)) {
+    case 4: 
+      EXPECT_EQ(buf_, "Hello, 0x00000000!\n");
+      break;
+    case 8:
+      EXPECT_EQ(buf_, "Hello, 0x0000000000000000!\n");
+      break;
+  }
+}
+
+TEST_F(PrintfTest, Octal) {
+  EXPECT_EQ(base_printf("Hello, %o!\n", 01234567), 16);
+  EXPECT_EQ(buf_, "Hello, 1234567!\n");
+}
+
+TEST_F(PrintfTest, Binary) {
+  EXPECT_EQ(base_printf("Hello, %b!\n", 0b1010'1010), 17);
+  EXPECT_EQ(buf_, "Hello, 10101010!\n");
+}
+
+TEST_F(PrintfTest, BinaryWithWidth) {
+  EXPECT_EQ(base_printf("Hello, %32b!\n", 0b1010'1010), 41);
+  EXPECT_EQ(buf_, "Hello, 00000000000000000000000010101010!\n");
+}
+
+TEST_F(PrintfTest, IncompleteSpec) {
+  base_printf("Hello, %");
+  EXPECT_THAT(buf_, StartsWith("Hello, "));
+}
+
+TEST_F(PrintfTest, UnknownSpec) {
+  base_printf("Hello, %j");
+  EXPECT_THAT(buf_, StartsWith("Hello, "));
+}
+
+TEST_F(PrintfTest, WidthTooNarrow) {
+  base_printf("Hello, %0x");
+  EXPECT_THAT(buf_, StartsWith("Hello, "));
+}
+
+TEST_F(PrintfTest, WidthTooWide) {
+  base_printf("Hello, %9001x");
+  EXPECT_THAT(buf_, StartsWith("Hello, "));
+}
+
+TEST_F(PrintfTest, ManySpecifiers) {
+  base_printf("%d + %d == %d, also spelled 0x%x", 2, 8, 2 + 8, 2 + 8);
+  EXPECT_THAT(buf_, StartsWith("2 + 8 == 10, also spelled 0xa"));
+}
+
+TEST(SnprintfTest, SimpleWrite) {
+  std::string buf(128, '\0');
+  auto len = base_snprintf(&buf[0], buf.size(), "Hello, World!\n");
+  buf.resize(len);
+  EXPECT_EQ(len, 14);
+  EXPECT_EQ(buf, "Hello, World!\n");
+}
+
+TEST(SnprintfTest, ComplexFormating) {
+  std::string buf(128, '\0');
+  auto len = base_snprintf(&buf[0], buf.size(), "%d + %d == %d, also spelled 0x%x", 2, 8, 2 + 8, 2 + 8);
+  buf.resize(len);
+  EXPECT_EQ(buf, "2 + 8 == 10, also spelled 0xa");
+}
+
+TEST(SnprintfTest, PartialWrite) {
+  std::string buf(16, '\0');
+  auto len = base_snprintf(&buf[0], buf.size(), "%d + %d == %d, also spelled 0x%x", 2, 8, 2 + 8, 2 + 8);
+  buf.resize(len);
+  EXPECT_EQ(len, 16);
+  EXPECT_EQ(buf, "2 + 8 == 10, als");
+}
+
+}  // namespace
+}  // namespace base
diff --git a/sw/device/lib/log_uart/log_impl.h b/sw/device/lib/log_uart/log_impl.h
index f43f4ca..d7eb2c9 100644
--- a/sw/device/lib/log_uart/log_impl.h
+++ b/sw/device/lib/log_uart/log_impl.h
@@ -5,7 +5,7 @@
 #ifndef OPENTITAN_SW_DEVICE_LIB_LOG_UART_LOG_IMPL_H_
 #define OPENTITAN_SW_DEVICE_LIB_LOG_UART_LOG_IMPL_H_
 
-#include "sw/device/lib/print_log.h"
+#include "sw/device/lib/base/print.h"
 #include "sw/device/lib/uart.h"
 
 // Stringify stuff.
@@ -52,6 +52,6 @@
  * this macro underneath) instead.
  */
 #define PRINT_LOG(log_header, ...) \
-  print_log(&uart_send_char, log_header __VA_ARGS__);
+  base_fprintf(uart_stdout, log_header __VA_ARGS__);
 
 #endif  // OPENTITAN_SW_DEVICE_LIB_LOG_UART_LOG_IMPL_H_
diff --git a/sw/device/lib/meson.build b/sw/device/lib/meson.build
index cacb167..839f1a7 100644
--- a/sw/device/lib/meson.build
+++ b/sw/device/lib/meson.build
@@ -14,6 +14,7 @@
     'uart_ot',
     sources: ['uart.c'],
     dependencies: [
+      sw_lib_base_print,
       sw_lib_runtime_ibex,
       dif_uart,
     ],
@@ -148,16 +149,3 @@
     ]
   )
 )
-
-# Logging library that prints to UART directly (sw_lib_log)
-sw_lib_log = declare_dependency(
-  link_with: static_library(
-    'log_ot',
-    sources: [
-      'print_log.c',
-    ],
-    dependencies: [
-      sw_lib_uart,
-    ]
-  )
-)
diff --git a/sw/device/lib/print_log.c b/sw/device/lib/print_log.c
deleted file mode 100644
index 302ed6f..0000000
--- a/sw/device/lib/print_log.c
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright lowRISC contributors.
-// Licensed under the Apache License, Version 2.0, see LICENSE for details.
-// SPDX-License-Identifier: Apache-2.0
-
-#include "sw/device/lib/print_log.h"
-
-#include <stdarg.h>
-
-// Identifiers for string format type specifiers.
-static const char kFormatSpecifier = '%';
-static const char kBinary = 'b';       // Print binary [SystemVerilog style].
-static const char kDecimalI = 'i';     // Signed decimal.
-static const char kDecimal = 'd';      // Signed decimal.
-static const char kUnsigned = 'u';     // Unsigned decimal.
-static const char kOctal = 'o';        // Octal.
-static const char kAsciiChar = 'c';    // Single character byte.
-static const char kHexUpper = 'X';     // Upper case hex.
-static const char kHexAltUpper = 'H';  // Upper case hex [SystemVerilog style].
-static const char kHexLower = 'x';     // Lower case hex.
-static const char kHexAltLower = 'h';  // Lower case hex [SystemVerilog style].
-static const char kPercent = '%';      // Escape '%'.
-
-static inline void print_digit(print_char_func print_char, unsigned int digit) {
-  print_char("0123456789ABCDEF"[digit]);
-}
-
-static inline void print_num(print_char_func print_char, int width,
-                             unsigned int n, unsigned int base) {
-  // TODO: Consider changing this to for loop.
-  if (--width > 0 || n >= base) {
-    print_num(print_char, width, n / base, base);
-  }
-  print_digit(print_char, n % base);
-}
-
-void print_log(print_char_func print_char, const char *fmt, ...) {
-  va_list va;
-  va_start(va, fmt);
-
-  while (*fmt != '\0') {
-    char ch = *fmt++;
-    if (ch != kFormatSpecifier) {
-      // Add CR to new line automatically (if not added already).
-      if (ch == '\n' && !(*(fmt - 1) == '\r' || *(fmt + 1) == '\r')) {
-        print_char('\r');
-      }
-      print_char(ch);
-    } else {
-      int w = 0;
-      // TODO: Refactor this into a separate function.
-      while (*fmt != '\0') {
-        ch = *fmt++;
-        // Parse width field.
-        if (ch >= '0' && ch <= '9') {
-          w = w * 10 + (ch - '0');
-          continue;
-        } else {
-          switch (ch) {
-            case '\0': {
-              return;
-            }
-            case kBinary: {
-              unsigned int n = va_arg(va, unsigned int);
-              print_num(print_char, w, n, 2);
-              break;
-            }
-            case kDecimalI:
-            case kDecimal: {
-              unsigned int n = va_arg(va, unsigned int);
-              if (((int)n) < 0) {
-                print_char('-');
-                n = -n;
-              }
-              print_num(print_char, w, n, 10);
-              break;
-            }
-            case kUnsigned: {
-              unsigned int n = va_arg(va, unsigned int);
-              print_num(print_char, w, n, 10);
-              break;
-            }
-            case kOctal: {
-              unsigned int n = va_arg(va, unsigned int);
-              print_num(print_char, w, n, 8);
-              break;
-            }
-            case kHexLower:
-            case kHexAltLower:
-            case kHexUpper:
-            case kHexAltUpper: {
-              // TODO: This will still print in upper case.
-              unsigned int n = va_arg(va, unsigned int);
-              print_num(print_char, w, n, 16);
-              break;
-            }
-            case kAsciiChar: {
-              char c = va_arg(va, int);
-              print_char(c);
-              break;
-            }
-            case kPercent: {
-              print_char('%');
-              break;
-            }
-            default: {
-              // Unknown format - this error message is printed inline within
-              // the message.
-              print_log(print_char, "[INVALID SPECIFIER: %%%c]", ch);
-              break;
-            }
-          }
-          break;
-        }
-      }
-    }
-  }
-  va_end(va);
-}
diff --git a/sw/device/lib/print_log.h b/sw/device/lib/print_log.h
deleted file mode 100644
index 4e1c6bd..0000000
--- a/sw/device/lib/print_log.h
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright lowRISC contributors.
-// Licensed under the Apache License, Version 2.0, see LICENSE for details.
-// SPDX-License-Identifier: Apache-2.0
-
-#ifndef OPENTITAN_SW_DEVICE_LIB_PRINT_LOG_H_
-#define OPENTITAN_SW_DEVICE_LIB_PRINT_LOG_H_
-
-// Pointer to a function that prints a character.
-typedef void (*print_char_func)(char);
-
-/**
- * Generic print log function that prints a format string with variable
- * number of type specifiers and arguments through a real hardware in the chip
- *
- * Function uses a format string as input which can contain a variable number
- * of type specifiers. These are fulfilled with with variable number of
- * corresponding arguments. It also takes a function pointer (which itself
- * takes a single char argument as input) as an argument to print (write) the
- * whole message string char by char through the HW IOs.
- *
- * To ensure portability of code across different platforms (DV simulations,
- * FPGA based emulations with  production SW, simulations using Verilator,
- * etc.), DO NOT CALL THIS FUNCTION DIRECTLY! Instead please use the generic
- * logging APIs defined in msg.h.
- * Also, the list of supported format specifiers is limited to integer types
- * (%c, %d, %x, %X).
- *
- * @param print_char: Function pointer that takes single character as input and
- *                    writes it to a HW in the chip such as UART for printing.
- * @param fmt:    Format string message with type specifiers. To maintain
- *                compatibility with the logging API implementation for DV, the
- *                type specifiers are limited to integer types.
- * @param ...:    Arguments passed to the format string based on the type
- *                specifiers in fmt.
- */
-void print_log(print_char_func print_char, const char *fmt, ...);
-
-#endif  // OPENTITAN_SW_DEVICE_LIB_PRINT_LOG_H_
diff --git a/sw/device/lib/uart.c b/sw/device/lib/uart.c
index 5ed7da7..9190bb2 100644
--- a/sw/device/lib/uart.c
+++ b/sw/device/lib/uart.c
@@ -34,6 +34,18 @@
   }
 }
 
+size_t uart_send_buf(void *data, const char *buf, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    uart_send_char(buf[i]);
+  }
+  return len;
+}
+
+const buffer_sink_t uart_stdout = {
+    .data = NULL,
+    .sink = &uart_send_buf,
+};
+
 #define hexchar(i) (((i & 0xf) > 9) ? (i & 0xf) - 10 + 'A' : (i & 0xf) + '0')
 
 void uart_send_uint(uint32_t n, int bits) {
diff --git a/sw/device/lib/uart.h b/sw/device/lib/uart.h
index 7a6fe3f..c35ab9b 100644
--- a/sw/device/lib/uart.h
+++ b/sw/device/lib/uart.h
@@ -5,8 +5,11 @@
 #ifndef OPENTITAN_SW_DEVICE_LIB_UART_H_
 #define OPENTITAN_SW_DEVICE_LIB_UART_H_
 
+#include <stddef.h>
 #include <stdint.h>
 
+#include "sw/device/lib/base/print.h"
+
 void uart_send_char(char c);
 
 /**
@@ -20,6 +23,8 @@
  */
 void uart_send_str(char *str);
 
+extern const buffer_sink_t uart_stdout;
+
 /**
  * Receive a single character from UART
  *