Add type-safe debugging macros for C.

These refactor the C++ bits slightly so that the library functions are
safe to use from C or C++ (without defining identifiers called things
like Bool in the global namespace) and provides some debug macros that
can be used from both C and C++, so that they can be used in headers.

As with the C++ versions, these use {} as the placeholders (no format
specifiers supported yet) and pass the type along with the value as an
argument.
diff --git a/sdk/core/allocator/token.h b/sdk/core/allocator/token.h
index 55f0385..1c75670 100644
--- a/sdk/core/allocator/token.h
+++ b/sdk/core/allocator/token.h
@@ -59,7 +59,7 @@
 	{
 		return {reinterpret_cast<uintptr_t>(
 		          static_cast<const volatile void *>(value)),
-		        DebugFormatArgument::Pointer};
+		        DebugFormatArgumentKind::DebugFormatArgumentPointer};
 	}
 };
 
diff --git a/sdk/include/__debug.h b/sdk/include/__debug.h
new file mode 100644
index 0000000..637e166
--- /dev/null
+++ b/sdk/include/__debug.h
@@ -0,0 +1,74 @@
+#pragma once
+#include <compartment-macros.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * The kind of value, for values that have special-cased handling.
+ */
+enum DebugFormatArgumentKind : ptraddr_t
+{
+	/// Boolean, printed as "true" or "false".
+	DebugFormatArgumentBool,
+	/// Single character.
+	DebugFormatArgumentCharacter,
+	/// Signed 32-bit integer, printed as decimal.
+	DebugFormatArgumentSignedNumber32,
+	/// Unsigned 32-bit integer, printed as hexadecimal
+	DebugFormatArgumentUnsignedNumber32,
+	/// Signed 64-bit integer, printed as decimal.
+	DebugFormatArgumentSignedNumber64,
+	/// Unsigned 64-bit integer, printed as hexadecimal.
+	DebugFormatArgumentUnsignedNumber64,
+	/// Pointer, printed as a full capability.
+	DebugFormatArgumentPointer,
+	/// Special case for permission sets, printed as in the capability
+	/// format.
+	DebugFormatArgumentPermissionSet,
+	/// C string, printed as-is.
+	DebugFormatArgumentCString,
+	/// String view, printed as-is.
+	DebugFormatArgumentStringView,
+};
+
+struct DebugFormatArgument
+{
+	/**
+	 * The value that is being written.
+	 */
+	uintptr_t value;
+	/**
+	 * The kind of value that is being written.  This is either a pointer
+	 * to a `DebugCallback` or one of the `Kind` enumeration, depending on
+	 * whether the tag is set or not.
+	 */
+	uintptr_t kind;
+};
+
+/**
+ * Library function that writes a debug message.  This runs with interrupts
+ * disabled (to avoid interleaving) and prints an array of debug messages. This
+ * is intended to allow a single call to print multiple format strings without
+ * requiring the format strings to be copied, so that the debugging APIs can
+ * wrap a user-provided format string.
+ */
+__cheri_libcall void
+debug_log_message_write(const char                 *context,
+                        const char                 *format,
+                        struct DebugFormatArgument *messages,
+                        size_t                      messageCount);
+
+/**
+ * Helper to write a debug message reporting an assertion or invariant failure.
+ * This should be used only with the helper macros / templates in `debug.h[h]`.
+ * This takes the kind of failure (for example, assert or invariant), the file,
+ * function, and line number where the failure occurred, a format string, and
+ * an array of arguments to the format string.
+ */
+__cheri_libcall void debug_report_failure(const char                 *kind,
+                                          const char                 *file,
+                                          const char                 *function,
+                                          int                         line,
+                                          const char                 *fmt,
+                                          struct DebugFormatArgument *arguments,
+                                          size_t argumentCount);
diff --git a/sdk/include/__macro_map.h b/sdk/include/__macro_map.h
new file mode 100644
index 0000000..ac507b7
--- /dev/null
+++ b/sdk/include/__macro_map.h
@@ -0,0 +1,69 @@
+/*
+ * Created by William Swanson in 2012.
+ *
+ * I, William Swanson, dedicate this work to the public domain.
+ * I waive all rights to the work worldwide under copyright law,
+ * including all related and neighboring rights,
+ * to the extent allowed by law.
+ *
+ * You can copy, modify, distribute and perform the work,
+ * even for commercial purposes, all without asking permission.
+ *
+ * CHERIoT modifications:
+ * - Added CHERIOT_ prefix to all macros to avoid namespace pollution
+ * - Used pragma once instead of include guards
+ */
+
+#pragma once
+
+#define CHERIOT_EVAL0(...) __VA_ARGS__
+#define CHERIOT_EVAL1(...)                                                     \
+	CHERIOT_EVAL0(CHERIOT_EVAL0(CHERIOT_EVAL0(__VA_ARGS__)))
+#define CHERIOT_EVAL2(...)                                                     \
+	CHERIOT_EVAL1(CHERIOT_EVAL1(CHERIOT_EVAL1(__VA_ARGS__)))
+#define CHERIOT_EVAL3(...)                                                     \
+	CHERIOT_EVAL2(CHERIOT_EVAL2(CHERIOT_EVAL2(__VA_ARGS__)))
+#define CHERIOT_EVAL4(...)                                                     \
+	CHERIOT_EVAL3(CHERIOT_EVAL3(CHERIOT_EVAL3(__VA_ARGS__)))
+#define CHERIOT_EVAL(...)                                                      \
+	CHERIOT_EVAL4(CHERIOT_EVAL4(CHERIOT_EVAL4(__VA_ARGS__)))
+
+#define CHERIOT_MAP_END(...)
+#define CHERIOT_MAP_OUT
+#define CHERIOT_MAP_COMMA ,
+
+#define CHERIOT_MAP_GET_END2() 0, CHERIOT_MAP_END
+#define CHERIOT_MAP_GET_END1(...) CHERIOT_MAP_GET_END2
+#define CHERIOT_MAP_GET_END(...) CHERIOT_MAP_GET_END1
+#define CHERIOT_MAP_NEXT0(test, next, ...) next CHERIOT_MAP_OUT
+#define CHERIOT_MAP_NEXT1(test, next) CHERIOT_MAP_NEXT0(test, next, 0)
+#define CHERIOT_MAP_NEXT(test, next)                                           \
+	CHERIOT_MAP_NEXT1(CHERIOT_MAP_GET_END test, next)
+
+#define CHERIOT_MAP0(f, x, peek, ...)                                          \
+	f(x) CHERIOT_MAP_NEXT(peek, CHERIOT_MAP1)(f, peek, __VA_ARGS__)
+#define CHERIOT_MAP1(f, x, peek, ...)                                          \
+	f(x) CHERIOT_MAP_NEXT(peek, CHERIOT_MAP0)(f, peek, __VA_ARGS__)
+
+#define CHERIOT_MAP_LIST_NEXT1(test, next)                                     \
+	CHERIOT_MAP_NEXT0(test, CHERIOT_MAP_COMMA next, 0)
+#define CHERIOT_MAP_LIST_NEXT(test, next)                                      \
+	CHERIOT_MAP_LIST_NEXT1(CHERIOT_MAP_GET_END test, next)
+
+#define CHERIOT_MAP_LIST0(f, x, peek, ...)                                     \
+	f(x) CHERIOT_MAP_LIST_NEXT(peek, CHERIOT_MAP_LIST1)(f, peek, __VA_ARGS__)
+#define CHERIOT_MAP_LIST1(f, x, peek, ...)                                     \
+	f(x) CHERIOT_MAP_LIST_NEXT(peek, CHERIOT_MAP_LIST0)(f, peek, __VA_ARGS__)
+
+/**
+ * Applies the function macro `f` to each of the remaining parameters.
+ */
+#define CHERIOT_MAP(f, ...)                                                    \
+	CHERIOT_EVAL(CHERIOT_MAP1(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0))
+
+/**
+ * Applies the function macro `f` to each of the remaining parameters and
+ * inserts commas between the results.
+ */
+#define CHERIOT_MAP_LIST(f, ...)                                               \
+	CHERIOT_EVAL(CHERIOT_MAP_LIST1(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0))
diff --git a/sdk/include/debug.h b/sdk/include/debug.h
new file mode 100644
index 0000000..9e6a56a
--- /dev/null
+++ b/sdk/include/debug.h
@@ -0,0 +1,139 @@
+#pragma once
+#include <__debug.h>
+#include <__macro_map.h>
+
+#ifndef __cplusplus
+
+/**
+ * Helper macro to convert the type of an argument to the corresponding
+ * `DebugFormatArgument` value.
+ *
+ * Should not be used directly.
+ */
+#	define CHERIOT_DEBUG_MAP_ARGUMENT(x)                                                \
+		{                                                                                \
+			(uintptr_t)(x), _Generic((x),                                              \
+                    _Bool: DebugFormatArgumentBool,                                               \
+                    char: DebugFormatArgumentCharacter,                                           \
+                    short: DebugFormatArgumentSignedNumber32,                                \
+                    unsigned short: DebugFormatArgumentUnsignedNumber32,                            \
+                    int: DebugFormatArgumentSignedNumber32,                                \
+                    unsigned int: DebugFormatArgumentUnsignedNumber32,                            \
+                    signed long long: DebugFormatArgumentSignedNumber64,                          \
+                    unsigned long long: DebugFormatArgumentUnsignedNumber64,                      \
+                    char *: DebugFormatArgumentCString, \
+					default: DebugFormatArgumentPointer) \
+		}
+
+/**
+ * Helper to map a list of arguments to an initialiser for a
+ * `DebugFormatArgument` array.
+ *
+ * Should not be used directly.
+ */
+#	define CHERIOT_DEBUG_MAP_ARGUMENTS(...)                                   \
+		CHERIOT_MAP_LIST(CHERIOT_DEBUG_MAP_ARGUMENT, __VA_ARGS__)
+
+/**
+ * Macro that logs a message.  The `context` argument is a string that is
+ * printed in magenta at the start of the line, followed by the format string.
+ * Each format argument is referenced with {} in the format string and is
+ * inserted in the output with the default rendering for that type.
+ */
+#	define CHERIOT_DEBUG_LOG(context, msg, ...)                               \
+		do                                                                     \
+		{                                                                      \
+			struct DebugFormatArgument args[] = {                              \
+			  __VA_OPT__(CHERIOT_DEBUG_MAP_ARGUMENTS(__VA_ARGS__))};           \
+			debug_log_message_write(                                           \
+			  context,                                                         \
+			  msg,                                                             \
+			  args,                                                            \
+			  0 __VA_OPT__(+(sizeof(args) / sizeof(args[0]))));                \
+		} while (0)
+
+/**
+ * Assert that `condition` is true, printing a message and aborting the
+ * compartment invocation if not.
+ */
+#	define CHERIOT_INVARIANT(condition, msg, ...)                             \
+		do                                                                     \
+		{                                                                      \
+			if (!(condition))                                                  \
+			{                                                                  \
+				struct DebugFormatArgument args[] = {                          \
+				  __VA_OPT__(CHERIOT_DEBUG_MAP_ARGUMENTS(__VA_ARGS__))};       \
+				debug_report_failure(                                          \
+				  "Invariant",                                                 \
+				  __FILE__,                                                    \
+				  __func__,                                                    \
+				  __LINE__,                                                    \
+				  msg,                                                         \
+				  args,                                                        \
+				  0 __VA_OPT__(+(sizeof(args) / sizeof(args[0]))));            \
+				__builtin_trap();                                              \
+			}                                                                  \
+		} while (0)
+
+#else
+#	include <debug.hh>
+
+namespace
+{
+	template<typename... Args>
+	__always_inline void
+	cheriot_debug_log(const char *context, const char *msg, Args... args)
+	{
+		DebugFormatArgument arguments[sizeof...(Args)];
+		make_debug_arguments_list(arguments, args...);
+		debug_log_message_write(context, msg, arguments, sizeof...(Args));
+	}
+
+	template<typename... Args>
+	__always_inline void cheriot_invariant(bool        condition,
+	                                       const char *file,
+	                                       const char *function,
+	                                       int         line,
+	                                       const char *msg,
+	                                       Args... args)
+	{
+		if (!condition)
+		{
+			DebugFormatArgument arguments[sizeof...(Args)];
+			make_debug_arguments_list(arguments, args...);
+			debug_report_failure("Invariant",
+			                     file,
+			                     function,
+			                     line,
+			                     msg,
+			                     arguments,
+			                     sizeof...(Args));
+		}
+	}
+} // namespace
+
+/**
+ * C++ version of `CHERIOT_DEBUG_LOG`.  This uses the C++ helpers and so will
+ * pretty-print a richer set of types than the C version.
+ */
+#	define CHERIOT_DEBUG_LOG(context, msg, ...)                               \
+		do                                                                     \
+		{                                                                      \
+			cheriot_debug_log(context, msg __VA_OPT__(, ) __VA_ARGS__);        \
+		} while (0)
+
+/**
+ * C++ version of `CHERIOT_INVARIANT`.  This uses the C++ helpers and so will
+ * pretty-print a richer set of types than the C version.
+ */
+#	define CHERIOT_INVARIANT(condition, msg, ...)                             \
+		do                                                                     \
+		{                                                                      \
+			cheriot_invariant(condition,                                       \
+			                  __FILE__,                                        \
+			                  __func__,                                        \
+			                  __LINE__,                                        \
+			                  msg __VA_OPT__(, ) __VA_ARGS__);                 \
+		} while (0)
+
+#endif
diff --git a/sdk/include/debug.hh b/sdk/include/debug.hh
index a95470b..21fb6b8 100644
--- a/sdk/include/debug.hh
+++ b/sdk/include/debug.hh
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: MIT
 
 #pragma once
+#include <__debug.h>
 #include <cheri.hh>
 #include <compartment.h>
 #include <concepts>
@@ -101,48 +102,6 @@
  */
 using DebugCallback = void (*)(uintptr_t, DebugWriter &);
 
-struct DebugFormatArgument
-{
-	/**
-	 * The kind of value, for values that have special-cased handling.
-	 */
-	enum Kind : ptraddr_t
-	{
-		/// Boolean, printed as "true" or "false".
-		Bool,
-		/// Single character.
-		Character,
-		/// Signed 32-bit integer, printed as decimal.
-		SignedNumber32,
-		/// Unsigned 32-bit integer, printed as hexadecimal
-		UnsignedNumber32,
-		/// Signed 64-bit integer, printed as decimal.
-		SignedNumber64,
-		/// Unsigned 64-bit integer, printed as hexadecimal.
-		UnsignedNumber64,
-		/// Pointer, printed as a full capability.
-		Pointer,
-		/// Special case for permission sets, printed as in the capability
-		/// format.
-		PermissionSet,
-		/// C string, printed as-is.
-		CString,
-		/// String view, printed as-is.
-		StringView,
-	};
-
-	/**
-	 * The value that is being written.
-	 */
-	uintptr_t value;
-	/**
-	 * The kind of value that is being written.  This is either a pointer
-	 * to a `DebugCallback` or one of the `Kind` enumeration, depending on
-	 * whether the tag is set or not.
-	 */
-	uintptr_t kind;
-};
-
 /**
  * Adaptor that turns an argument of type `T` into a `DebugFormatArgument`.
  *
@@ -160,7 +119,8 @@
 {
 	__always_inline static DebugFormatArgument construct(bool value)
 	{
-		return {static_cast<uintptr_t>(value), DebugFormatArgument::Bool};
+		return {static_cast<uintptr_t>(value),
+		        DebugFormatArgumentKind::DebugFormatArgumentBool};
 	}
 };
 
@@ -172,7 +132,8 @@
 {
 	__always_inline static DebugFormatArgument construct(char value)
 	{
-		return {static_cast<uintptr_t>(value), DebugFormatArgument::Character};
+		return {static_cast<uintptr_t>(value),
+		        DebugFormatArgumentKind::DebugFormatArgumentCharacter};
 	}
 };
 
@@ -185,7 +146,7 @@
 	__always_inline static DebugFormatArgument construct(uint8_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::UnsignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32};
 	}
 };
 
@@ -198,7 +159,7 @@
 	__always_inline static DebugFormatArgument construct(uint16_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::UnsignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32};
 	}
 };
 
@@ -211,7 +172,7 @@
 	__always_inline static DebugFormatArgument construct(uint32_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::UnsignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32};
 	}
 };
 
@@ -229,7 +190,8 @@
 	{
 		uintptr_t fudgedValue;
 		memcpy(&fudgedValue, &value, sizeof(fudgedValue));
-		return {fudgedValue, DebugFormatArgument::UnsignedNumber64};
+		return {fudgedValue,
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber64};
 	}
 };
 
@@ -242,7 +204,7 @@
 	__always_inline static DebugFormatArgument construct(int8_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::SignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32};
 	}
 };
 
@@ -255,7 +217,7 @@
 	__always_inline static DebugFormatArgument construct(int16_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::SignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32};
 	}
 };
 
@@ -268,7 +230,7 @@
 	__always_inline static DebugFormatArgument construct(int32_t value)
 	{
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::SignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32};
 	}
 };
 
@@ -287,7 +249,8 @@
 		static_assert(sizeof(uintptr_t) == sizeof(uint64_t));
 		uintptr_t fudgedValue;
 		memcpy(&fudgedValue, &value, sizeof(fudgedValue));
-		return {fudgedValue, DebugFormatArgument::SignedNumber64};
+		return {fudgedValue,
+		        DebugFormatArgumentKind::DebugFormatArgumentSignedNumber64};
 	}
 };
 
@@ -300,7 +263,7 @@
 	__always_inline static DebugFormatArgument construct(const char *value)
 	{
 		return {reinterpret_cast<uintptr_t>(value),
-		        DebugFormatArgument::CString};
+		        DebugFormatArgumentKind::DebugFormatArgumentCString};
 	}
 };
 
@@ -317,7 +280,7 @@
 	construct(std::string_view &value)
 	{
 		return {reinterpret_cast<uintptr_t>(&value),
-		        DebugFormatArgument::StringView};
+		        DebugFormatArgumentKind::DebugFormatArgumentStringView};
 	}
 };
 
@@ -334,7 +297,7 @@
 	{
 #ifdef CHERIOT_AVOID_CAPRELOCS
 		return {static_cast<uintptr_t>(value),
-		        DebugFormatArgument::UnsignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32};
 #else
 		return {static_cast<uintptr_t>(value),
 		        reinterpret_cast<uintptr_t>(&debug_enum_helper<T>)};
@@ -352,7 +315,19 @@
 	construct(CHERI::PermissionSet value)
 	{
 		return {static_cast<uintptr_t>(value.as_raw()),
-		        DebugFormatArgument::PermissionSet};
+		        DebugFormatArgumentKind::DebugFormatArgumentPermissionSet};
+	}
+};
+
+/**
+ * Null pointer specialisation.
+ */
+template<>
+struct DebugFormatArgumentAdaptor<std::nullptr_t>
+{
+	__always_inline static DebugFormatArgument construct(std::nullptr_t)
+	{
+		return {0, DebugFormatArgumentKind::DebugFormatArgumentPointer};
 	}
 };
 
@@ -366,7 +341,7 @@
 	{
 		return {reinterpret_cast<uintptr_t>(
 		          static_cast<const volatile void *>(value)),
-		        DebugFormatArgument::Pointer};
+		        DebugFormatArgumentKind::DebugFormatArgumentPointer};
 	}
 };
 
@@ -382,7 +357,7 @@
 	{
 		return {reinterpret_cast<uintptr_t>(
 		          static_cast<const volatile void *>(value)),
-		        DebugFormatArgument::Pointer};
+		        DebugFormatArgumentKind::DebugFormatArgumentPointer};
 	}
 };
 
@@ -397,7 +372,7 @@
 	{
 		return {static_cast<uintptr_t>(value),
 
-		        DebugFormatArgument::UnsignedNumber32};
+		        DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32};
 	}
 };
 
diff --git a/sdk/lib/debug/debug.cc b/sdk/lib/debug/debug.cc
index 123bf59..0756164 100644
--- a/sdk/lib/debug/debug.cc
+++ b/sdk/lib/debug/debug.cc
@@ -257,48 +257,58 @@
 					else
 					{
 						switch (
-						  static_cast<DebugFormatArgument::Kind>(argument.kind))
+						  static_cast<DebugFormatArgumentKind>(argument.kind))
 						{
-							case DebugFormatArgument::Kind::Bool:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentBool:
 								write(static_cast<bool>(argument.value)
 								        ? "true"
 								        : "false");
 								break;
-							case DebugFormatArgument::Kind::Character:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentCharacter:
 								write(static_cast<char>(argument.value));
 								break;
-							case DebugFormatArgument::Kind::Pointer:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentPointer:
 								write(reinterpret_cast<void *>(argument.value));
 								break;
-							case DebugFormatArgument::Kind::SignedNumber32:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentSignedNumber32:
 								write(static_cast<int32_t>(argument.value));
 								break;
-							case DebugFormatArgument::Kind::UnsignedNumber32:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentUnsignedNumber32:
 								write(static_cast<uint32_t>(argument.value));
 								break;
-							case DebugFormatArgument::Kind::SignedNumber64:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentSignedNumber64:
 							{
 								int64_t value;
 								memcpy(&value, &argument.value, sizeof(value));
 								write(value);
 								break;
 							}
-							case DebugFormatArgument::Kind::UnsignedNumber64:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentUnsignedNumber64:
 							{
 								uint64_t value;
 								memcpy(&value, &argument.value, sizeof(value));
 								write(value);
 								break;
 							}
-							case DebugFormatArgument::Kind::CString:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentCString:
 								write(reinterpret_cast<const char *>(
 								  argument.value));
 								break;
-							case DebugFormatArgument::Kind::StringView:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentStringView:
 								write(*reinterpret_cast<std::string_view *>(
 								  argument.value));
 								break;
-							case DebugFormatArgument::Kind::PermissionSet:
+							case DebugFormatArgumentKind::
+							  DebugFormatArgumentPermissionSet:
 								write(CHERI::PermissionSet::from_raw(
 								  argument.value));
 								break;
diff --git a/tests/debug-test.c b/tests/debug-test.c
new file mode 100644
index 0000000..03c008c
--- /dev/null
+++ b/tests/debug-test.c
@@ -0,0 +1,25 @@
+#include <compartment.h>
+#include <debug.h>
+
+__cheri_compartment("debug_test") void test_debug_c()
+{
+	unsigned char x = 'c';
+	_Bool         t = true;
+	CHERIOT_DEBUG_LOG(
+	  "Debug messages",
+	  "Testing C debug log: 42:{}, true:{}, hello world:{}, "
+	  "'c':{}, &x:{}, NULL:{}, short 3:{}, unsigned short 0xf:{}",
+	  42,
+	  t,
+	  "hello world",
+	  (char)'c',
+	  &x,
+	  NULL,
+	  (short)3,
+	  (unsigned short)0xf);
+	// Just test that these compile:
+	CHERIOT_INVARIANT(true, "Testing C++ invariant failure: 42:{}", 42);
+	CHERIOT_INVARIANT(true, "Testing C++ invariant failure");
+	CHERIOT_INVARIANT(
+	  true, "Testing C++ invariant failure: 42:{}", 42, 1, 3, 4, "oops");
+}
diff --git a/tests/debug-test.cc b/tests/debug-test.cc
new file mode 100644
index 0000000..e44bb65
--- /dev/null
+++ b/tests/debug-test.cc
@@ -0,0 +1,22 @@
+#include "tests.hh"
+#include <compartment.h>
+#include <debug.h>
+
+void test_debug_cxx()
+{
+	unsigned char x = 'c';
+	CHERIOT_DEBUG_LOG("Debug messages",
+	                  "Testing C++ debug log: 42:{}, true:{}, hello world:{}, "
+	                  "'c':{}, &x:{}, nullptr:{}",
+	                  42,
+	                  true,
+	                  "hello world",
+	                  'c',
+	                  &x,
+	                  nullptr);
+	// Just test that these compile:
+	CHERIOT_INVARIANT(true, "Testing C++ invariant failure: 42:{}", 42);
+	CHERIOT_INVARIANT(true, "Testing C++ invariant failure");
+	CHERIOT_INVARIANT(
+	  true, "Testing C++ invariant failure: 42:{}", 42, 1, 3, 4, "oops");
+}
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index b59a823..8d99fdd 100644
--- a/tests/test-runner.cc
+++ b/tests/test-runner.cc
@@ -107,6 +107,8 @@
 	debug_log("Trying to print string: {}", std::string_view{testString, 13});
 
 	run_timed("All tests", []() {
+		run_timed("Debug helpers (C++)", test_debug_cxx);
+		run_timed("Debug helpers (C)", test_debug_c);
 		run_timed("MMIO", test_mmio);
 		run_timed("stdio", test_stdio);
 		run_timed("Static sealing", test_static_sealing);
diff --git a/tests/tests.hh b/tests/tests.hh
index ffb71d0..bd63a3c 100644
--- a/tests/tests.hh
+++ b/tests/tests.hh
@@ -20,6 +20,8 @@
 __cheri_compartment("misc_test") void test_misc();
 __cheri_compartment("static_sealing_test") void test_static_sealing();
 __cheri_compartment("stdio_test") void test_stdio();
+__cheri_compartment("debug_test") void test_debug_cxx();
+__cheri_compartment("debug_test") void test_debug_c();
 
 // Simple tests don't need a separate compartment.
 void test_global_constructors();
diff --git a/tests/xmake.lua b/tests/xmake.lua
index 48c294a..2f9e5ed 100644
--- a/tests/xmake.lua
+++ b/tests/xmake.lua
@@ -54,6 +54,9 @@
 test("queue")
 -- Test minimal stdio implementation
 test("stdio")
+-- Test the debug helpers.
+test("debug")
+	add_files("debug-test.c")
 -- Test the static sealing types
 test("static_sealing")
 compartment("static_sealing_inner")
@@ -112,6 +115,7 @@
     add_deps("check_pointer_test")
     add_deps("misc_test")
     add_deps("stdio_test")
+    add_deps("debug_test")
     -- Set the thread entry point to the test runner.
     on_load(function(target)
         target:values_set("board", "$(board)")