Make printf use the platform UART.

For historical reasons, this had its own hard-coded version of a 16550.

Doing this required making the file C++.  At the same time, fix a load
of style issue and replace a void* and function pointer with the
function wrapper from the network stack.  This provides a type-erased
way of calling a lambda, so we can clean up the code a bit.

Also remove ambient Uart access from this compartment and move the UART
access into the header.
diff --git a/sdk/include/function_wrapper.hh b/sdk/include/function_wrapper.hh
new file mode 100644
index 0000000..50ca248
--- /dev/null
+++ b/sdk/include/function_wrapper.hh
@@ -0,0 +1,109 @@
+#include <cdefs.h>
+#include <functional>
+#include <tuple>
+
+/**
+ * Base template for `FunctionWrapper`, never used.
+ */
+template<typename FnType>
+class FunctionWrapper;
+
+/**
+ * A non-owning type-erased reference to a callable object.  This is used
+ * to pass lambdas (and similar) down the stack without increasing code
+ * size by template specialisation.  Instances of this class must not be
+ * stored, they should be used only for passing type-erased callable objects
+ * down the stack.
+ *
+ * Instances of this class are two words: a reference to the lambda, and a
+ * wrapper callback that invokes the lambda.
+ *
+ * This is similar to `std::function` but is non-owning and so is guaranteed
+ * not to allocate memory (the called function may capture memory).
+ */
+template<class R, class... Args>
+class FunctionWrapper<R(Args...)>
+{
+	/**
+	 * Storage for the type-erased function.  This holds the reference to
+	 * lambda and to the invoke function.
+	 */
+	alignas(void *) char storage[2 * sizeof(void *)];
+
+	/**
+	 * Base type for the type-erased function.  This defines the virtual
+	 * function that is used to invoke the captured lambda.
+	 */
+	struct ErasedFunctionWrapperBase
+	{
+		virtual R operator()(Args... args) = 0;
+	};
+
+	/**
+	 * Returns a pointer to the storage, cast to the type-erased function
+	 * type.
+	 */
+	ErasedFunctionWrapperBase &stored_function()
+	{
+		return *reinterpret_cast<ErasedFunctionWrapperBase *>(storage);
+	}
+
+	/**
+	 * Templated subclass that is specialised for each concrete callable
+	 * type `T` that is passed.  One instance of this will be created for
+	 * each lambda type, with a single method in its vtable that invokes
+	 * the lambda.
+	 */
+	template<typename T>
+	class ErasedFunctionWrapper : public ErasedFunctionWrapperBase
+	{
+		/// Pointer to the captured lambda.
+		T &&fn;
+
+		public:
+		/**
+		 * Invoke function.  This is virtual and overrides the version in
+		 * the parent class, allowing this to be called from code that does
+		 * not know the cocrete type of the lambda.
+		 */
+		R operator()(Args... args) override
+		{
+			return fn(std::forward<Args>(args)...);
+		}
+
+		/**
+		 * Construct the type-erased function wrapper, capturing the
+		 * lambda.
+		 */
+		ErasedFunctionWrapper(T &&fn) : fn{std::forward<T>(fn)} {}
+	};
+
+	public:
+	/**
+	 * This is a non-owning reference, delete its copy and move
+	 * constructors to avoid accidental copies.
+	 */
+	FunctionWrapper(FunctionWrapper &)  = delete;
+	FunctionWrapper(FunctionWrapper &&) = delete;
+	FunctionWrapper &operator=(FunctionWrapper &&) = delete;
+
+	/**
+	 * Construct the type-erased function wrapper, capturing the lambda.
+	 */
+	template<typename T>
+	__always_inline FunctionWrapper(T &&fn)
+	{
+		// Make sure that we got the size for the storage right!
+		static_assert(sizeof(storage) >= sizeof(ErasedFunctionWrapper<T>));
+		// Construct the type-erased function in place.
+		new (storage) ErasedFunctionWrapper<T>(std::forward<T>(fn));
+	}
+
+	/**
+	 * Invoke the captured lambda.
+	 */
+	__always_inline R operator()(Args... args)
+	{
+		return stored_function()(std::forward<Args>(args)...);
+	}
+};
diff --git a/sdk/include/stdio.h b/sdk/include/stdio.h
index 4d15320..fd6b605 100644
--- a/sdk/include/stdio.h
+++ b/sdk/include/stdio.h
@@ -5,6 +5,7 @@
 #define __STDIO_H__
 
 #include <cdefs.h>
+#include <compartment-macros.h>
 #include <stdarg.h>
 #include <stddef.h>
 
@@ -12,13 +13,56 @@
 #define EOF (-1)
 
 __BEGIN_DECLS
-int __cheri_libcall printf(const char *fmt, ...);
 
-#define name_printf(fmt, ...)                                                  \
-	printf(__XSTRING(__CHERI_COMPARTMENT__) ": " fmt, ##__VA_ARGS__)
+/**
+ * This is a very simple implementation of a subset of stdio and supports only
+ * UARTs.  The Uart type is often a C++ template type and so we can't forward
+ * declare it in a C header and so we use a volatile void* instead, which can
+ * be cast to the correct type inside the library.
+ */
+typedef volatile void FILE;
+
+#if DEVICE_EXISTS(uart0)
+#	define stdout MMIO_CAPABILITY(void, uart0)
+#	define stdin MMIO_CAPABILITY(void, uart0)
+#elif DEVICE_EXISTS(uart)
+#	define stdout MMIO_CAPABILITY(void, uart)
+#	define stdin MMIO_CAPABILITY(void uart)
+#endif
+
+#if DEVICE_EXISTS(uart1)
+#	define stderr MMIO_CAPABILITY(void, uart1)
+#elif defined(stdout)
+#	define stderr stdout
+#endif
+
+int __cheri_libcall vfprintf(FILE *stream, const char *fmt, va_list ap);
+
+static inline int fprintf(FILE *stream, const char *format, ...)
+{
+	va_list ap;
+
+	va_start(ap, format);
+	int ret = vfprintf(stream, format, ap);
+	va_end(ap);
+	return ret;
+}
+
+static inline int printf(const char *format, ...)
+{
+	va_list ap;
+
+	va_start(ap, format);
+	int ret = vfprintf(stdout, format, ap);
+	va_end(ap);
+	return ret;
+}
 
 int __cheri_libcall snprintf(char *str, size_t size, const char *format, ...);
-int __cheri_libcall vsnprintf(char *str, size_t size, const char *format, va_list ap); 
+int __cheri_libcall vsnprintf(const char *str,
+                              size_t      size,
+                              const char *format,
+                              va_list     ap);
 __END_DECLS
 
 #endif /* !__STDIO_H__ */
diff --git a/sdk/lib/stdio/printf.c b/sdk/lib/stdio/printf.c
deleted file mode 100644
index fc210a1..0000000
--- a/sdk/lib/stdio/printf.c
+++ /dev/null
@@ -1,558 +0,0 @@
-/*-
- * Copyright (c) 1986, 1988, 1991, 1993
- *	The Regents of the University of California.  All rights reserved.
- * (c) UNIX System Laboratories, Inc.
- * All or some portions of this file are derived from material licensed
- * to the University of California by American Telephone and Telegraph
- * Co. or Unix System Laboratories, Inc. and are reproduced herein with
- * the permission of UNIX System Laboratories, Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 4. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- *
- *	@(#)subr_prf.c	8.3 (Berkeley) 1/21/94
- */
-
-/* __FBSDID("$FreeBSD: stable/8/sys/kern/subr_prf.c 210305 2010-07-20 18:55:13Z
- * jkim $"); */
-
-#include <cdefs.h>
-#include <cheri-builtins.h>
-#include <compartment.h>
-#include <inttypes.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-
-/*
- * Definitions snarfed from various parts of the FreeBSD headers:
- */
-#define toupper(c) ((c)-0x20 * (((c) >= 'a') && ((c) <= 'z')))
-
-static char hex2ascii(uintmax_t in)
-{
-	return (in < 10) ? '0' + in : 'a' + in - 10;
-}
-
-/* Max number conversion buffer length: a u_quad_t in base 2, plus NUL byte. */
-#define MAXNBUF (sizeof(intmax_t) * CHAR_BIT + 1)
-
-struct snprintf_arg
-{
-	char * str;
-	size_t remain;
-};
-
-static void snprintf_func(int ch, void *arg)
-{
-	struct snprintf_arg *const info = arg;
-
-	if (info->remain >= 2)
-	{
-		*info->str++ = ch;
-		info->remain--;
-	}
-}
-
-/*
- * Put a NUL-terminated ASCII number (base <= 36) in a buffer in reverse
- * order; return an optional length and a pointer to the last character
- * written in the buffer (i.e., the first character of the string).
- * The buffer pointed to by `nbuf' must have length >= MAXNBUF.
- */
-// FIXME: Using `unsigned` for `num` instead of `uintmax_t` means that we are
-// going to truncate large numbers, but it avoids needing a library routine to
-// handle division.
-static char *ksprintn(char *nbuf, unsigned num, int base, int *lenp, int upper)
-{
-	char *p, c;
-
-	p  = nbuf;
-	*p = '\0';
-	do
-	{
-		c    = hex2ascii(num % base);
-		*++p = upper ? toupper(c) : c;
-	} while (num /= base);
-	if (lenp)
-		*lenp = p - nbuf;
-	return (p);
-}
-
-/*
- * Scaled down version of printf(3).
- *
- * Two additional formats:
- *
- * The format %b is supported to decode error registers.
- * Its usage is:
- *
- *	printf("reg=%b\n", regval, "<base><arg>*");
- *
- * where <base> is the output base expressed as a control character, e.g.
- * \10 gives octal; \20 gives hex.  Each arg is a sequence of characters,
- * the first of which gives the bit number to be inspected (origin 1), and
- * the next characters (up to a control character, i.e. a character <= 32),
- * give the name of the register.  Thus:
- *
- *	kvprintf("reg=%b\n", 3, "\10\2BITTWO\1BITONE\n");
- *
- * would produce output:
- *
- *	reg=3<BITTWO,BITONE>
- *
- * %D  -- Hexdump, takes pointer and separator string:
- *		("%6D", ptr, ":")   -> XX:XX:XX:XX:XX:XX
- *		("%*D", len, ptr, " " -> XX XX XX XX ...
- */
-static int kvprintf(char const *fmt,
-                    void (*func)(int, void *),
-                    void *  arg,
-                    int     radix,
-                    va_list ap)
-{
-#define PCHAR(c)                                                               \
-	{                                                                          \
-		int cc = (c);                                                          \
-		if (func)                                                              \
-			(*func)(cc, arg);                                                  \
-		else                                                                   \
-			*d++ = cc;                                                         \
-		retval++;                                                              \
-	}
-	char           nbuf[MAXNBUF];
-	char *         d;
-	const char *   p, *percent, *q;
-	unsigned char *up;
-	int            ch, n;
-	uintmax_t      num;
-	int  base, lflag, qflag, tmp, width, ladjust, sharpflag, neg, sign, dot;
-	int  cflag, hflag, jflag, tflag, zflag;
-	int  dwidth, upper;
-	char padc;
-	int  stop = 0, retval = 0;
-	char null_str[1] = {'\0'};
-
-	num = 0;
-	if (!func)
-		d = (char *)arg;
-	else
-		d = NULL;
-
-	if (fmt == NULL)
-		fmt = null_str;
-
-	if (radix < 2 || radix > 36)
-		radix = 10;
-
-	for (;;)
-	{
-		padc  = ' ';
-		width = 0;
-		while ((ch = (unsigned char)*fmt++) != '%' || stop)
-		{
-			if (ch == '\0')
-				return (retval);
-			PCHAR(ch);
-		}
-		percent   = fmt - 1;
-		qflag     = 0;
-		lflag     = 0;
-		ladjust   = 0;
-		sharpflag = 0;
-		neg       = 0;
-		sign      = 0;
-		dot       = 0;
-		dwidth    = 0;
-		upper     = 0;
-		cflag     = 0;
-		hflag     = 0;
-		jflag     = 0;
-		tflag     = 0;
-		zflag     = 0;
-	reswitch:
-		switch (ch = (unsigned char)*fmt++)
-		{
-			case '.':
-				dot = 1;
-				goto reswitch;
-			case '#':
-				sharpflag = 1;
-				goto reswitch;
-			case '+':
-				sign = 1;
-				goto reswitch;
-			case '-':
-				ladjust = 1;
-				goto reswitch;
-			case '%':
-				PCHAR(ch);
-				break;
-			case '*':
-				if (!dot)
-				{
-					width = va_arg(ap, int);
-					if (width < 0)
-					{
-						ladjust = !ladjust;
-						width   = -width;
-					}
-				}
-				else
-				{
-					dwidth = va_arg(ap, int);
-				}
-				goto reswitch;
-			case '0':
-				if (!dot)
-				{
-					padc = '0';
-					goto reswitch;
-				}
-			case '1':
-			case '2':
-			case '3':
-			case '4':
-			case '5':
-			case '6':
-			case '7':
-			case '8':
-			case '9':
-				for (n = 0;; ++fmt)
-				{
-					n  = n * 10 + ch - '0';
-					ch = *fmt;
-					if (ch < '0' || ch > '9')
-						break;
-				}
-				if (dot)
-					dwidth = n;
-				else
-					width = n;
-				goto reswitch;
-			case 'b':
-				num = (unsigned int)va_arg(ap, int);
-				p   = va_arg(ap, char *);
-				for (q = ksprintn(nbuf, num, *p++, NULL, 0); *q;)
-					PCHAR(*q--);
-
-				if (num == 0)
-					break;
-
-				for (tmp = 0; *p;)
-				{
-					n = *p++;
-					if (num & (1 << (n - 1)))
-					{
-						PCHAR(tmp ? ',' : '<');
-						for (; (n = *p) > ' '; ++p)
-							PCHAR(n);
-						tmp = 1;
-					}
-					else
-						for (; *p > ' '; ++p)
-							continue;
-				}
-				if (tmp)
-					PCHAR('>');
-				break;
-			case 'c':
-				PCHAR(va_arg(ap, int));
-				break;
-			case 'D':
-				up = va_arg(ap, unsigned char *);
-				p  = va_arg(ap, char *);
-				if (!width)
-					width = 16;
-				while (width--)
-				{
-					PCHAR(hex2ascii(*up >> 4));
-					PCHAR(hex2ascii(*up & 0x0f));
-					up++;
-					if (width)
-						for (q = p; *q; q++)
-							PCHAR(*q);
-				}
-				break;
-			case 'd':
-			case 'i':
-				base = 10;
-				sign = 1;
-				goto handle_sign;
-			case 'h':
-				if (hflag)
-				{
-					hflag = 0;
-					cflag = 1;
-				}
-				else
-					hflag = 1;
-				goto reswitch;
-			case 'j':
-				jflag = 1;
-				goto reswitch;
-			case 'l':
-				if (lflag)
-				{
-					lflag = 0;
-					qflag = 1;
-				}
-				else
-					lflag = 1;
-				goto reswitch;
-			case 'n':
-				if (jflag)
-					*(va_arg(ap, intmax_t *)) = retval;
-				else if (qflag)
-					*(va_arg(ap, long long *)) = retval;
-				else if (lflag)
-					*(va_arg(ap, long *)) = retval;
-				else if (zflag)
-					*(va_arg(ap, size_t *)) = retval;
-				else if (hflag)
-					*(va_arg(ap, short *)) = retval;
-				else if (cflag)
-					*(va_arg(ap, char *)) = retval;
-				else
-					*(va_arg(ap, int *)) = retval;
-				break;
-			case 'o':
-				base = 8;
-				goto handle_nosign;
-			case 'p':
-				base      = 16;
-				sharpflag = (width == 0);
-				sign      = 0;
-				num       = (size_t)va_arg(ap, void *);
-				goto number;
-			case 'q':
-				qflag = 1;
-				goto reswitch;
-			case 'r':
-				base = radix;
-				if (sign)
-					goto handle_sign;
-				goto handle_nosign;
-			case 's':
-				p = va_arg(ap, char *);
-				if (p == NULL)
-					p = null_str;
-				if (!dot)
-					n = strlen(p);
-				else
-					for (n = 0; n < dwidth && p[n]; n++)
-						continue;
-
-				width -= n;
-
-				if (!ladjust && width > 0)
-					while (width--)
-						PCHAR(padc);
-				while (n--)
-					PCHAR(*p++);
-				if (ladjust && width > 0)
-					while (width--)
-						PCHAR(padc);
-				break;
-			case 't':
-				tflag = 1;
-				goto reswitch;
-			case 'u':
-				base = 10;
-				goto handle_nosign;
-			case 'X':
-				upper = 1;
-			case 'x':
-				base = 16;
-				goto handle_nosign;
-			case 'y':
-				base = 16;
-				sign = 1;
-				goto handle_sign;
-			case 'z':
-				zflag = 1;
-				goto reswitch;
-			handle_nosign:
-				sign = 0;
-				if (jflag)
-					num = va_arg(ap, uintmax_t);
-				else if (qflag)
-					num = va_arg(ap, unsigned long long);
-				else if (tflag)
-					num = va_arg(ap, ptrdiff_t);
-				else if (lflag)
-					num = va_arg(ap, unsigned long);
-				else if (zflag)
-					num = va_arg(ap, size_t);
-				else if (hflag)
-					num = (unsigned short)va_arg(ap, int);
-				else if (cflag)
-					num = (unsigned char)va_arg(ap, int);
-				else
-					num = va_arg(ap, unsigned int);
-				goto number;
-			handle_sign:
-				if (jflag)
-					num = va_arg(ap, intmax_t);
-				else if (qflag)
-					num = va_arg(ap, long long);
-				else if (tflag)
-					num = va_arg(ap, ptrdiff_t);
-				else if (lflag)
-					num = va_arg(ap, long);
-				else if (zflag)
-					num = va_arg(ap, ssize_t);
-				else if (hflag)
-					num = (short)va_arg(ap, int);
-				else if (cflag)
-					num = (char)va_arg(ap, int);
-				else
-					num = va_arg(ap, int);
-			number:
-				if (sign && (intmax_t)num < 0)
-				{
-					neg = 1;
-					num = -(intmax_t)num;
-				}
-				p   = ksprintn(nbuf, num, base, &n, upper);
-				tmp = 0;
-				if (sharpflag && num != 0)
-				{
-					if (base == 8)
-						tmp++;
-					else if (base == 16)
-						tmp += 2;
-				}
-				if (neg)
-					tmp++;
-
-				if (!ladjust && padc == '0')
-					dwidth = width - tmp;
-				width -= tmp + (dwidth > n ? dwidth : n);
-				dwidth -= n;
-				if (!ladjust)
-					while (width-- > 0)
-						PCHAR(' ');
-				if (neg)
-					PCHAR('-');
-				if (sharpflag && num != 0)
-				{
-					if (base == 8)
-					{
-						PCHAR('0');
-					}
-					else if (base == 16)
-					{
-						PCHAR('0');
-						PCHAR('x');
-					}
-				}
-				while (dwidth-- > 0)
-					PCHAR('0');
-
-				while (*p)
-					PCHAR(*p--);
-
-				if (ladjust)
-					while (width-- > 0)
-						PCHAR(' ');
-
-				break;
-			default:
-				while (percent < fmt)
-					PCHAR(*percent++);
-				/*
-				 * Since we ignore an formatting argument it is no
-				 * longer safe to obey the remaining formatting
-				 * arguments as the arguments will no longer match
-				 * the format specs.
-				 */
-				stop = 1;
-				break;
-		}
-	}
-#undef PCHAR
-}
-
-/*
- * Scaled down version of vsnprintf(3).
- */
-int __cheri_libcall vsnprintf(char *str, size_t size, const char *format, va_list ap)
-{
-	struct snprintf_arg info;
-	int                 retval;
-
-	info.str    = str;
-	info.remain = size;
-	retval      = kvprintf(format, snprintf_func, &info, 10, ap);
-	if (info.remain >= 1)
-		*info.str++ = '\0';
-	return (retval);
-}
-
-#define UART_REG_QUEUE 0
-#define UART_REG_LINESTAT (5)
-#define UART_REG_STATUS_RX (0x01)
-#define UART_REG_STATUS_TX (0x20)
-
-
-[[cheri::interrupt_state(disabled)]]
-static void uart16550_txbuffer(const char *ptr)
-{
-	long               flags;
-	volatile uint32_t *uart16550 = MMIO_CAPABILITY(uint32_t, uart);
-
-	for (int i = 0; i < PRT_MAX_SIZE && ptr[i]; i++)
-	{
-		while ((uart16550[UART_REG_LINESTAT] & UART_REG_STATUS_TX) == 0) {}
-
-		uart16550[UART_REG_QUEUE] = ptr[i];
-	}
-}
-
-int __cheri_libcall printf(const char *format, ...)
-{
-	char    buf[PRT_MAX_SIZE];
-	int     rv;
-	va_list ap;
-
-	va_start(ap, format);
-	rv = vsnprintf(buf, PRT_MAX_SIZE, format, ap);
-	va_end(ap);
-	uart16550_txbuffer(buf);
-
-	return rv;
-}
-
-int __cheri_libcall snprintf(char *str, size_t size, const char *format, ...)
-{
-	int     rv;
-	va_list ap;
-
-	va_start(ap, format);
-	rv = vsnprintf(str, size, format, ap);
-	va_end(ap);
-
-	return rv;
-}
diff --git a/sdk/lib/stdio/printf.cc b/sdk/lib/stdio/printf.cc
new file mode 100644
index 0000000..8fc5f51
--- /dev/null
+++ b/sdk/lib/stdio/printf.cc
@@ -0,0 +1,552 @@
+/*-
+ * Copyright (c) 1986, 1988, 1991, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ * (c) UNIX System Laboratories, Inc.
+ * All or some portions of this file are derived from material licensed
+ * to the University of California by American Telephone and Telegraph
+ * Co. or Unix System Laboratories, Inc. and are reproduced herein with
+ * the permission of UNIX System Laboratories, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 4. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)subr_prf.c	8.3 (Berkeley) 1/21/94
+ */
+
+/* __FBSDID("$FreeBSD: stable/8/sys/kern/subr_prf.c 210305 2010-07-20 18:55:13Z
+ * jkim $"); */
+
+#include <cdefs.h>
+#include <cheri-builtins.h>
+#include <compartment.h>
+#include <cstdint>
+#include <function_wrapper.hh>
+#include <inttypes.h>
+#include <platform-uart.hh>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+/*
+ * Definitions snarfed from various parts of the FreeBSD headers:
+ */
+namespace
+{
+	__always_inline char toupper(char c)
+	{
+		return ((c)-0x20 * (((c) >= 'a') && ((c) <= 'z')));
+	}
+
+	static char hex2ascii(uintmax_t in)
+	{
+		return (in < 10) ? '0' + in : 'a' + in - 10;
+	}
+
+/* Max number conversion buffer length: a u_quad_t in base 2, plus NUL byte. */
+#define MAXNBUF (sizeof(intmax_t) * CHAR_BIT + 1)
+
+	/*
+	 * Put a NUL-terminated ASCII number (base <= 36) in a buffer in reverse
+	 * order; return an optional length and a pointer to the last character
+	 * written in the buffer (i.e., the first character of the string).
+	 * The buffer pointed to by `nbuf' must have length >= MAXNBUF.
+	 */
+	// FIXME: Using `unsigned` for `num` instead of `uintmax_t` means that we
+	// are going to truncate large numbers, but it avoids needing a library
+	// routine to handle division.
+	static char *
+	ksprintn(char *nbuf, unsigned num, int base, int *lenp, int upper)
+	{
+		char *p, c;
+
+		p  = nbuf;
+		*p = '\0';
+		do
+		{
+			c    = hex2ascii(num % base);
+			*++p = upper ? toupper(c) : c;
+		} while (num /= base);
+		if (lenp)
+			*lenp = p - nbuf;
+		return (p);
+	}
+
+	/*
+	 * Scaled down version of printf(3).
+	 *
+	 * Two additional formats:
+	 *
+	 * The format %b is supported to decode error registers.
+	 * Its usage is:
+	 *
+	 *	printf("reg=%b\n", regval, "<base><arg>*");
+	 *
+	 * where <base> is the output base expressed as a control character, e.g.
+	 * \10 gives octal; \20 gives hex.  Each arg is a sequence of characters,
+	 * the first of which gives the bit number to be inspected (origin 1), and
+	 * the next characters (up to a control character, i.e. a character <= 32),
+	 * give the name of the register.  Thus:
+	 *
+	 *	kvprintf("reg=%b\n", 3, "\10\2BITTWO\1BITONE\n");
+	 *
+	 * would produce output:
+	 *
+	 *	reg=3<BITTWO,BITONE>
+	 *
+	 * %D  -- Hexdump, takes pointer and separator string:
+	 *		("%6D", ptr, ":")   -> XX:XX:XX:XX:XX:XX
+	 *		("%*D", len, ptr, " " -> XX XX XX XX ...
+	 */
+	__noinline int kvprintf(char const                *fmt,
+	                        FunctionWrapper<void(int)> func,
+	                        void                      *arg,
+	                        int                        radix,
+	                        va_list                    ap)
+	{
+		char           nbuf[MAXNBUF];
+		const char    *p, *percent, *q;
+		unsigned char *up;
+		int            ch, n;
+		uintmax_t      num;
+		int  base, lflag, qflag, tmp, width, ladjust, sharpflag, neg, sign, dot;
+		int  cflag, hflag, jflag, tflag, zflag;
+		int  dwidth, upper;
+		char padc;
+		int  stop = 0, retval = 0;
+		const char *emptyString = "";
+
+		auto putchar = [&](int c) {
+			func(c);
+			retval++;
+		};
+
+		if (fmt == nullptr)
+		{
+			fmt = emptyString;
+		}
+
+		if (radix < 2 || radix > 36)
+		{
+			radix = 10;
+		}
+
+		for (;;)
+		{
+			padc  = ' ';
+			width = 0;
+			while ((ch = static_cast<unsigned char>(*fmt++)) != '%' || stop)
+			{
+				if (ch == '\0')
+					return (retval);
+				putchar(ch);
+			}
+			percent   = fmt - 1;
+			qflag     = 0;
+			lflag     = 0;
+			ladjust   = 0;
+			sharpflag = 0;
+			neg       = 0;
+			sign      = 0;
+			dot       = 0;
+			dwidth    = 0;
+			upper     = 0;
+			cflag     = 0;
+			hflag     = 0;
+			jflag     = 0;
+			tflag     = 0;
+			zflag     = 0;
+		reswitch:
+			switch (ch = static_cast<unsigned char>(*fmt++))
+			{
+				case '.':
+					dot = 1;
+					goto reswitch; // NOLINT
+				case '#':
+					sharpflag = 1;
+					goto reswitch; // NOLINT
+				case '+':
+					sign = 1;
+					goto reswitch; // NOLINT
+				case '-':
+					ladjust = 1;
+					goto reswitch; // NOLINT
+				case '%':
+					putchar(ch);
+					break;
+				case '*':
+					if (!dot)
+					{
+						width = va_arg(ap, int);
+						if (width < 0)
+						{
+							ladjust = !ladjust;
+							width   = -width;
+						}
+					}
+					else
+					{
+						dwidth = va_arg(ap, int);
+					}
+					goto reswitch; // NOLINT
+				case '0':
+					if (!dot)
+					{
+						padc = '0';
+						goto reswitch; // NOLINT
+					}
+				case '1':
+				case '2':
+				case '3':
+				case '4':
+				case '5':
+				case '6':
+				case '7':
+				case '8':
+				case '9':
+					for (n = 0;; ++fmt)
+					{
+						n  = n * 10 + ch - '0';
+						ch = *fmt;
+						if (ch < '0' || ch > '9')
+							break;
+					}
+					if (dot)
+						dwidth = n;
+					else
+						width = n;
+					goto reswitch; // NOLINT
+				case 'b':
+					num = static_cast<unsigned int>(va_arg(ap, int));
+					p   = va_arg(ap, char *);
+					for (q = ksprintn(nbuf, num, *p++, nullptr, 0); *q;)
+						putchar(*q--);
+
+					if (num == 0)
+						break;
+
+					for (tmp = 0; *p;)
+					{
+						n = *p++;
+						if (num & (1 << (n - 1)))
+						{
+							putchar(tmp ? ',' : '<');
+							for (; (n = *p) > ' '; ++p)
+								putchar(n);
+							tmp = 1;
+						}
+						else
+							for (; *p > ' '; ++p)
+								continue;
+					}
+					if (tmp)
+						putchar('>');
+					break;
+				case 'c':
+					putchar(va_arg(ap, int));
+					break;
+				case 'D':
+					up = va_arg(ap, unsigned char *);
+					p  = va_arg(ap, char *);
+					if (!width)
+						width = 16;
+					while (width--)
+					{
+						putchar(hex2ascii(*up >> 4));
+						putchar(hex2ascii(*up & 0x0f));
+						up++;
+						if (width)
+							for (q = p; *q; q++)
+								putchar(*q);
+					}
+					break;
+				case 'd':
+				case 'i':
+					base = 10;
+					sign = 1;
+					goto handle_sign; // NOLINT
+				case 'h':
+					if (hflag)
+					{
+						hflag = 0;
+						cflag = 1;
+					}
+					else
+						hflag = 1;
+					goto reswitch; // NOLINT
+				case 'j':
+					jflag = 1;
+					goto reswitch; // NOLINT
+				case 'l':
+					if (lflag)
+					{
+						lflag = 0;
+						qflag = 1;
+					}
+					else
+						lflag = 1;
+					goto reswitch; // NOLINT
+				case 'n':
+					if (jflag)
+						*(va_arg(ap, intmax_t *)) = retval;
+					else if (qflag)
+						*(va_arg(ap, long long *)) = retval;
+					else if (lflag)
+						*(va_arg(ap, long *)) = retval;
+					else if (zflag)
+						*(va_arg(ap, size_t *)) = retval;
+					else if (hflag)
+						*(va_arg(ap, short *)) = static_cast<short>(retval);
+					else if (cflag)
+						*(va_arg(ap, char *)) = retval;
+					else
+						*(va_arg(ap, int *)) = retval;
+					break;
+				case 'o':
+					base = 8;
+					goto handle_nosign; // NOLINT
+				case 'p':
+					base      = 16;
+					sharpflag = (width == 0);
+					sign      = 0;
+					num       = static_cast<size_t>(
+                      reinterpret_cast<uintptr_t>(va_arg(ap, void *)));
+					goto number; // NOLINT
+				case 'q':
+					qflag = 1;
+					goto reswitch; // NOLINT
+				case 'r':
+					base = radix;
+					if (sign)
+						goto handle_sign; // NOLINT
+					goto handle_nosign;   // NOLINT
+				case 's':
+					p = va_arg(ap, char *);
+					if (p == nullptr)
+					{
+						p = emptyString;
+					}
+					if (!dot)
+					{
+						n = strlen(p);
+					}
+					else
+					{
+						for (n = 0; n < dwidth && p[n]; n++)
+						{
+							continue;
+						}
+					}
+
+					width -= n;
+
+					if (!ladjust && width > 0)
+					{
+						while (width--)
+						{
+							putchar(padc);
+						}
+					}
+					while (n--)
+					{
+						putchar(*p++);
+					}
+					if (ladjust && width > 0)
+					{
+						while (width--)
+						{
+							putchar(padc);
+						}
+					}
+					break;
+				case 't':
+					tflag = 1;
+					goto reswitch; // NOLINT
+				case 'u':
+					base = 10;
+					goto handle_nosign; // NOLINT
+				case 'X':
+					upper = 1;
+				case 'x':
+					base = 16;
+					goto handle_nosign; // NOLINT
+				case 'y':
+					base = 16;
+					sign = 1;
+					goto handle_sign; // NOLINT
+				case 'z':
+					zflag = 1;
+					goto reswitch; // NOLINT
+				handle_nosign:
+					sign = 0;
+					if (jflag)
+						num = va_arg(ap, uintmax_t);
+					else if (qflag)
+						num = va_arg(ap, unsigned long long);
+					else if (tflag)
+						num = va_arg(ap, ptrdiff_t);
+					else if (lflag)
+						num = va_arg(ap, unsigned long);
+					else if (zflag)
+						num = va_arg(ap, size_t);
+					else if (hflag)
+						num = static_cast<unsigned short>(va_arg(ap, int));
+					else if (cflag)
+						num = static_cast<unsigned char>(va_arg(ap, int));
+					else
+						num = va_arg(ap, unsigned int);
+					goto number; // NOLINT
+				handle_sign:
+					if (jflag)
+						num = va_arg(ap, intmax_t);
+					else if (qflag)
+						num = va_arg(ap, long long);
+					else if (tflag)
+						num = va_arg(ap, ptrdiff_t);
+					else if (lflag)
+						num = va_arg(ap, long);
+					else if (zflag)
+						num = va_arg(ap, ssize_t);
+					else if (hflag)
+						num = static_cast<short>(va_arg(ap, int));
+					else if (cflag)
+						num = static_cast<char>(va_arg(ap, int));
+					else
+						num = va_arg(ap, int);
+				number:
+					if (sign && static_cast<intmax_t>(num) < 0)
+					{
+						neg = 1;
+						num = -static_cast<intmax_t>(num);
+					}
+					p   = ksprintn(nbuf, num, base, &n, upper);
+					tmp = 0;
+					if (sharpflag && num != 0)
+					{
+						if (base == 8)
+							tmp++;
+						else if (base == 16)
+							tmp += 2;
+					}
+					if (neg)
+						tmp++;
+
+					if (!ladjust && padc == '0')
+						dwidth = width - tmp;
+					width -= tmp + (dwidth > n ? dwidth : n);
+					dwidth -= n;
+					if (!ladjust)
+						while (width-- > 0)
+							putchar(' ');
+					if (neg)
+						putchar('-');
+					if (sharpflag && num != 0)
+					{
+						if (base == 8)
+						{
+							putchar('0');
+						}
+						else if (base == 16)
+						{
+							putchar('0');
+							putchar('x');
+						}
+					}
+					while (dwidth-- > 0)
+						putchar('0');
+
+					while (*p)
+						putchar(*p--);
+
+					if (ladjust)
+						while (width-- > 0)
+							putchar(' ');
+
+					break;
+				default:
+					while (percent < fmt)
+						putchar(*percent++);
+					/*
+					 * Since we ignore an formatting argument it is no
+					 * longer safe to obey the remaining formatting
+					 * arguments as the arguments will no longer match
+					 * the format specs.
+					 */
+					stop = 1;
+					break;
+			}
+		}
+	}
+
+} // namespace
+
+/*
+ * Scaled down version of vsnprintf(3).
+ */
+int __cheri_libcall
+vsnprintf(char *str, // NOLINT (clang-tidy spuriously thinks this should be
+                     // const, even though it's being written to)
+          size_t      size,
+          const char *format,
+          va_list     ap)
+{
+	struct Buffer
+	{
+		char  *str;
+		size_t remain;
+	} info        = {str, size};
+	auto callback = [&](int ch) {
+		if (info.remain >= 2)
+		{
+			*info.str++ = ch;
+			info.remain--;
+		}
+	};
+	int retval = kvprintf(format, callback, &info, 10, ap);
+	if (info.remain >= 1)
+		*info.str++ = '\0';
+	return (retval);
+}
+
+[[cheri::interrupt_state(disabled)]] int __cheri_libcall
+vfprintf(FILE *stream, const char *fmt, va_list ap)
+{
+	return kvprintf(
+	  fmt,
+	  [=](int ch) { static_cast<volatile Uart *>(stream)->blocking_write(ch); },
+	  nullptr,
+	  10,
+	  ap);
+}
+
+int __cheri_libcall snprintf(char *str, size_t size, const char *format, ...)
+{
+	int     rv;
+	va_list ap;
+
+	va_start(ap, format);
+	rv = vsnprintf(str, size, format, ap);
+	va_end(ap);
+
+	return rv;
+}
diff --git a/sdk/lib/stdio/xmake.lua b/sdk/lib/stdio/xmake.lua
index 01e39cc..2596268 100644
--- a/sdk/lib/stdio/xmake.lua
+++ b/sdk/lib/stdio/xmake.lua
@@ -1,4 +1,4 @@
 library("stdio")
   add_deps("string")
   set_default(false)
-  add_files("printf.c")
+  add_files("printf.cc")
diff --git a/tests/stdio-test.cc b/tests/stdio-test.cc
new file mode 100644
index 0000000..a23aa0b
--- /dev/null
+++ b/tests/stdio-test.cc
@@ -0,0 +1,22 @@
+#include <cstdio>
+#define TEST_NAME "stdio"
+#include "tests.hh"
+#include <stdio.h>
+
+void test_stdio()
+{
+	debug_log("Printing 'Hello, world!' to stdout");
+	printf("Hello, world!\n");
+	debug_log("Printing 'Hello, world!' to stderr");
+	fprintf(stderr, "Hello, world!\n");
+	const size_t BufferSize = 64;
+	char         buffer[BufferSize];
+	snprintf(buffer, BufferSize, "%d", 42);
+	TEST(strcmp(buffer, "42") == 0,
+	     "snprintf(\"%d\", 42) gave {}",
+	     std::string_view{buffer, BufferSize});
+	snprintf(buffer, BufferSize, "%d", -42);
+	TEST(strcmp(buffer, "-42") == 0,
+	     "snprintf(\"%d\", -42) gave {}",
+	     std::string_view{buffer, BufferSize});
+}
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index d96a8e0..b59a823 100644
--- a/tests/test-runner.cc
+++ b/tests/test-runner.cc
@@ -108,6 +108,7 @@
 
 	run_timed("All tests", []() {
 		run_timed("MMIO", test_mmio);
+		run_timed("stdio", test_stdio);
 		run_timed("Static sealing", test_static_sealing);
 		run_timed("Crash recovery", test_crash_recovery);
 		run_timed("Compartment calls", test_compartment_call);
diff --git a/tests/tests.hh b/tests/tests.hh
index 4fec070..ffb71d0 100644
--- a/tests/tests.hh
+++ b/tests/tests.hh
@@ -19,6 +19,7 @@
 __cheri_compartment("check_pointer_test") void test_check_pointer();
 __cheri_compartment("misc_test") void test_misc();
 __cheri_compartment("static_sealing_test") void test_static_sealing();
+__cheri_compartment("stdio_test") void test_stdio();
 
 // Simple tests don't need a separate compartment.
 void test_global_constructors();
diff --git a/tests/xmake.lua b/tests/xmake.lua
index 4cb7213..48c294a 100644
--- a/tests/xmake.lua
+++ b/tests/xmake.lua
@@ -49,9 +49,11 @@
 -- Test the futex implementation
 test("futex")
 -- Test locks built on top of the futex
-test("queue")
--- Test queues
 test("locks")
+-- Test queues
+test("queue")
+-- Test minimal stdio implementation
+test("stdio")
 -- Test the static sealing types
 test("static_sealing")
 compartment("static_sealing_inner")
@@ -92,6 +94,7 @@
     -- Helper libraries
     add_deps("freestanding", "string", "crt", "cxxrt", "atomic_fixed", "compartment_helpers", "debug")
     add_deps("message_queue", "locks", "event_group")
+    add_deps("stdio")
     -- Tests
     add_deps("mmio_test")
     add_deps("eventgroup_test")
@@ -108,6 +111,7 @@
     add_deps("compartment_calls_test", "compartment_calls_inner", "compartment_calls_inner_with_handler")
     add_deps("check_pointer_test")
     add_deps("misc_test")
+    add_deps("stdio_test")
     -- Set the thread entry point to the test runner.
     on_load(function(target)
         target:values_set("board", "$(board)")