summaryrefslogtreecommitdiff
path: root/libmisc
diff options
context:
space:
mode:
Diffstat (limited to 'libmisc')
-rw-r--r--libmisc/CMakeLists.txt29
-rw-r--r--libmisc/assert.c21
-rw-r--r--libmisc/endian.c136
-rw-r--r--libmisc/error.c26
-rw-r--r--libmisc/error_generated.c186
-rwxr-xr-xlibmisc/error_generated.c.gen35
-rw-r--r--libmisc/fmt.c266
-rw-r--r--libmisc/hash.c24
-rw-r--r--libmisc/include/libmisc/_intercept.h24
-rw-r--r--libmisc/include/libmisc/alloc.h26
-rw-r--r--libmisc/include/libmisc/assert.h17
-rw-r--r--libmisc/include/libmisc/endian.h144
-rw-r--r--libmisc/include/libmisc/error.h167
-rw-r--r--libmisc/include/libmisc/fmt.h163
-rw-r--r--libmisc/include/libmisc/hash.h24
-rw-r--r--libmisc/include/libmisc/linkedlist.h108
-rw-r--r--libmisc/include/libmisc/log.h32
-rw-r--r--libmisc/include/libmisc/macro.h187
-rw-r--r--libmisc/include/libmisc/map.h145
-rw-r--r--libmisc/include/libmisc/obj.h164
-rw-r--r--libmisc/include/libmisc/private.h32
-rw-r--r--libmisc/include/libmisc/rand.h34
-rw-r--r--libmisc/include/libmisc/utf8.h25
-rw-r--r--libmisc/include/libmisc/vcall.h28
-rw-r--r--libmisc/intercept.c20
-rw-r--r--libmisc/linkedlist.c64
-rw-r--r--libmisc/log.c294
-rw-r--r--libmisc/map.c230
-rw-r--r--libmisc/rand.c38
-rw-r--r--libmisc/tests/test_assert.c86
-rw-r--r--libmisc/tests/test_assert_min.c17
-rw-r--r--libmisc/tests/test_endian.c39
-rw-r--r--libmisc/tests/test_fmt.c243
-rw-r--r--libmisc/tests/test_log.c81
-rw-r--r--libmisc/tests/test_macro.c191
-rw-r--r--libmisc/tests/test_map.c60
-rw-r--r--libmisc/tests/test_obj.c61
-rw-r--r--libmisc/tests/test_obj_nest.c73
-rw-r--r--libmisc/tests/test_private.c10
-rw-r--r--libmisc/tests/test_rand.c30
-rw-r--r--libmisc/tests/test_vcall.c74
-rw-r--r--libmisc/utf8.c44
-rwxr-xr-xlibmisc/wrap-cc186
43 files changed, 3433 insertions, 451 deletions
diff --git a/libmisc/CMakeLists.txt b/libmisc/CMakeLists.txt
index 02b19d5..9bb282b 100644
--- a/libmisc/CMakeLists.txt
+++ b/libmisc/CMakeLists.txt
@@ -1,20 +1,39 @@
-# libmisc/CMakeLists.txt - A simple Go-ish object system built on GCC -fplan9-extensions
+# libmisc/CMakeLists.txt - Low-level C programming utilities; sort of an augmented "libc"
#
-# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
add_library(libmisc INTERFACE)
-target_include_directories(libmisc SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
+target_include_directories(libmisc PUBLIC INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_sources(libmisc INTERFACE
assert.c
+ endian.c
+ error.c
+ error_generated.c
+ fmt.c
+ hash.c
+ intercept.c
+ linkedlist.c
log.c
+ map.c
+ rand.c
+ utf8.c
+)
+
+target_compile_options(libmisc INTERFACE
+ -no-integrated-cpp
+ -wrapper "${CMAKE_CURRENT_SOURCE_DIR}/wrap-cc"
)
-target_compile_options(libmisc INTERFACE "$<$<COMPILE_LANGUAGE:C>:-fplan9-extensions>")
add_lib_test(libmisc test_assert)
+add_lib_test(libmisc test_assert_min)
add_lib_test(libmisc test_endian)
+add_lib_test(libmisc test_fmt)
add_lib_test(libmisc test_hash)
add_lib_test(libmisc test_log)
+add_lib_test(libmisc test_macro)
+add_lib_test(libmisc test_map)
+add_lib_test(libmisc test_obj)
+add_lib_test(libmisc test_obj_nest)
add_lib_test(libmisc test_private)
add_lib_test(libmisc test_rand)
-add_lib_test(libmisc test_vcall)
diff --git a/libmisc/assert.c b/libmisc/assert.c
index 8231c85..410ec21 100644
--- a/libmisc/assert.c
+++ b/libmisc/assert.c
@@ -1,31 +1,26 @@
/* libmisc/assert.c - More assertions
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-#include <stdbool.h> /* for bool, true, false */
-#include <stdlib.h> /* for abort() */
-
#define LOG_NAME ASSERT
-#include <libmisc/log.h> /* for errorf() */
+#include <libmisc/log.h> /* for log_errorln() */
#include <libmisc/assert.h>
#ifndef NDEBUG
-[[noreturn, gnu::weak]]
void __assert_msg_fail(const char *expr,
- const char *file, unsigned int line, const char *func,
- const char *msg) {
+ const char *file, unsigned int line, const char *func,
+ const char *msg) {
static bool in_fail = false;
if (!in_fail) {
in_fail = true;
- errorf("%s:%u:%s(): assertion \"%s\" failed%s%s",
- file, line, func,
- expr,
- msg ? ": " : "", msg ?: "");
+ log_errorln(file, ":", line, ":", func, "(): ",
+ "assertion ", (qstr, expr), " failed",
+ msg ? ": " : "", msg ?: "");
in_fail = false;
}
- abort();
+ __lm_abort();
}
#endif
diff --git a/libmisc/endian.c b/libmisc/endian.c
new file mode 100644
index 0000000..2528f48
--- /dev/null
+++ b/libmisc/endian.c
@@ -0,0 +1,136 @@
+/* libmisc/endian.c - Endian-conversion helpers
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/macro.h> /* for LM_FORCE_SEMICOLON */
+
+#include <libmisc/endian.h>
+
+#define endian_declare_wrappers(NBIT, ENDIAN) \
+ uint##NBIT##ENDIAN##_t uint##NBIT##ENDIAN##_marshal(uint##NBIT##_t in) { \
+ uint##NBIT##ENDIAN##_t out; \
+ uint##NBIT##ENDIAN##_encode(out.octets, in); \
+ return out; \
+ } \
+ uint##NBIT##_t uint##NBIT##ENDIAN##_unmarshal(uint##NBIT##ENDIAN##_t in) { \
+ return uint##NBIT##ENDIAN##_decode(in.octets); \
+ } \
+ LM_FORCE_SEMICOLON
+
+/* Big endian *****************************************************************/
+
+size_t uint16be_encode(uint8_t *out, uint16_t in) {
+ out[0] = (uint8_t)((in >> 8) & 0xFF);
+ out[1] = (uint8_t)((in >> 0) & 0xFF);
+ return 2;
+}
+
+uint16_t uint16be_decode(uint8_t *in) {
+ return (((uint16_t)(in[0])) << 8)
+ | (((uint16_t)(in[1])) << 0)
+ ;
+}
+
+size_t uint32be_encode(uint8_t *out, uint32_t in) {
+ out[0] = (uint8_t)((in >> 24) & 0xFF);
+ out[1] = (uint8_t)((in >> 16) & 0xFF);
+ out[2] = (uint8_t)((in >> 8) & 0xFF);
+ out[3] = (uint8_t)((in >> 0) & 0xFF);
+ return 4;
+}
+
+uint32_t uint32be_decode(uint8_t *in) {
+ return (((uint32_t)(in[0])) << 24)
+ | (((uint32_t)(in[1])) << 16)
+ | (((uint32_t)(in[2])) << 8)
+ | (((uint32_t)(in[3])) << 0)
+ ;
+}
+
+size_t uint64be_encode(uint8_t *out, uint64_t in) {
+ out[0] = (uint8_t)((in >> 56) & 0xFF);
+ out[1] = (uint8_t)((in >> 48) & 0xFF);
+ out[2] = (uint8_t)((in >> 40) & 0xFF);
+ out[3] = (uint8_t)((in >> 32) & 0xFF);
+ out[4] = (uint8_t)((in >> 24) & 0xFF);
+ out[5] = (uint8_t)((in >> 16) & 0xFF);
+ out[6] = (uint8_t)((in >> 8) & 0xFF);
+ out[7] = (uint8_t)((in >> 0) & 0xFF);
+ return 8;
+}
+
+uint64_t uint64be_decode(uint8_t *in) {
+ return (((uint64_t)(in[0])) << 56)
+ | (((uint64_t)(in[1])) << 48)
+ | (((uint64_t)(in[2])) << 40)
+ | (((uint64_t)(in[3])) << 32)
+ | (((uint64_t)(in[4])) << 24)
+ | (((uint64_t)(in[5])) << 16)
+ | (((uint64_t)(in[6])) << 8)
+ | (((uint64_t)(in[7])) << 0)
+ ;
+}
+
+endian_declare_wrappers(16, be);
+endian_declare_wrappers(32, be);
+endian_declare_wrappers(64, be);
+
+/* Little endian **************************************************************/
+
+size_t uint16le_encode(uint8_t *out, uint16_t in) {
+ out[0] = (uint8_t)((in >> 0) & 0xFF);
+ out[1] = (uint8_t)((in >> 8) & 0xFF);
+ return 2;
+}
+
+uint16_t uint16le_decode(uint8_t *in) {
+ return (((uint16_t)(in[0])) << 0)
+ | (((uint16_t)(in[1])) << 8)
+ ;
+}
+
+size_t uint32le_encode(uint8_t *out, uint32_t in) {
+ out[0] = (uint8_t)((in >> 0) & 0xFF);
+ out[1] = (uint8_t)((in >> 8) & 0xFF);
+ out[2] = (uint8_t)((in >> 16) & 0xFF);
+ out[3] = (uint8_t)((in >> 24) & 0xFF);
+ return 4;
+}
+
+uint32_t uint32le_decode(uint8_t *in) {
+ return (((uint32_t)(in[0])) << 0)
+ | (((uint32_t)(in[1])) << 8)
+ | (((uint32_t)(in[2])) << 16)
+ | (((uint32_t)(in[3])) << 24)
+ ;
+}
+
+size_t uint64le_encode(uint8_t *out, uint64_t in) {
+ out[0] = (uint8_t)((in >> 0) & 0xFF);
+ out[1] = (uint8_t)((in >> 8) & 0xFF);
+ out[2] = (uint8_t)((in >> 16) & 0xFF);
+ out[3] = (uint8_t)((in >> 24) & 0xFF);
+ out[4] = (uint8_t)((in >> 32) & 0xFF);
+ out[5] = (uint8_t)((in >> 40) & 0xFF);
+ out[6] = (uint8_t)((in >> 48) & 0xFF);
+ out[7] = (uint8_t)((in >> 56) & 0xFF);
+ return 8;
+}
+
+uint64_t uint64le_decode(uint8_t *in) {
+ return (((uint64_t)(in[0])) << 0)
+ | (((uint64_t)(in[1])) << 8)
+ | (((uint64_t)(in[2])) << 16)
+ | (((uint64_t)(in[3])) << 24)
+ | (((uint64_t)(in[4])) << 32)
+ | (((uint64_t)(in[5])) << 40)
+ | (((uint64_t)(in[6])) << 48)
+ | (((uint64_t)(in[7])) << 56)
+ ;
+}
+
+endian_declare_wrappers(16, le);
+endian_declare_wrappers(32, le);
+endian_declare_wrappers(64, le);
diff --git a/libmisc/error.c b/libmisc/error.c
new file mode 100644
index 0000000..dfe4e80
--- /dev/null
+++ b/libmisc/error.c
@@ -0,0 +1,26 @@
+/* libmisc/error.c - Go-esque errors
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/error.h>
+
+const char *error_msg(error err) {
+ return (err._msg && err._msg[0])
+ ? err._msg
+ : _errnum_str_msg(err.num);
+}
+
+void error_cleanup(error *errptr) {
+ if (!errptr)
+ return;
+ if (errptr->_msg)
+ free(errptr->_msg);
+ errptr->num = E_NOERROR;
+ errptr->_msg = NULL;
+}
+
+void fmt_print_error(lo_interface fmt_dest w, error err) {
+ fmt_print(w, (str, error_msg(err)), " (", _errnum_str_sym(err.num), ")");
+}
diff --git a/libmisc/error_generated.c b/libmisc/error_generated.c
new file mode 100644
index 0000000..e0afcab
--- /dev/null
+++ b/libmisc/error_generated.c
@@ -0,0 +1,186 @@
+/* libmisc/error_generated.c - Generated by libmisc/error_generated.c.gen. DO NOT EDIT! */
+
+#include <libmisc/error.h>
+
+const char *_errnum_str_sym(_errnum errnum) {
+ switch (errnum) {
+ case E_NOERROR: return "E_NOERROR";
+ case E_EOF: return "E_EOF";
+ case E_NET_EARP_TIMEOUT: return "E_NET_EARP_TIMEOUT";
+ case E_NET_EACK_TIMEOUT: return "E_NET_EACK_TIMEOUT";
+ case E_NET_ERECV_TIMEOUT: return "E_NET_ERECV_TIMEOUT";
+ case E_NET_ECLOSED: return "E_NET_ECLOSED";
+ case E_POSIX_E2BIG: return "E_POSIX_E2BIG";
+ case E_POSIX_EACCES: return "E_POSIX_EACCES";
+ case E_POSIX_EADDRINUSE: return "E_POSIX_EADDRINUSE";
+ case E_POSIX_EADDRNOTAVAIL: return "E_POSIX_EADDRNOTAVAIL";
+ case E_POSIX_EAFNOSUPPORT: return "E_POSIX_EAFNOSUPPORT";
+ case E_POSIX_EAGAIN: return "E_POSIX_EAGAIN";
+ case E_POSIX_EALREADY: return "E_POSIX_EALREADY";
+ case E_POSIX_EBADF: return "E_POSIX_EBADF";
+ case E_POSIX_EBADMSG: return "E_POSIX_EBADMSG";
+ case E_POSIX_EBUSY: return "E_POSIX_EBUSY";
+ case E_POSIX_ECANCELED: return "E_POSIX_ECANCELED";
+ case E_POSIX_ECHILD: return "E_POSIX_ECHILD";
+ case E_POSIX_ECONNABORTED: return "E_POSIX_ECONNABORTED";
+ case E_POSIX_ECONNREFUSED: return "E_POSIX_ECONNREFUSED";
+ case E_POSIX_ECONNRESET: return "E_POSIX_ECONNRESET";
+ case E_POSIX_EDEADLK: return "E_POSIX_EDEADLK";
+ case E_POSIX_EDESTADDRREQ: return "E_POSIX_EDESTADDRREQ";
+ case E_POSIX_EDOM: return "E_POSIX_EDOM";
+ case E_POSIX_EDQUOT: return "E_POSIX_EDQUOT";
+ case E_POSIX_EEXIST: return "E_POSIX_EEXIST";
+ case E_POSIX_EFAULT: return "E_POSIX_EFAULT";
+ case E_POSIX_EFBIG: return "E_POSIX_EFBIG";
+ case E_POSIX_EHOSTUNREACH: return "E_POSIX_EHOSTUNREACH";
+ case E_POSIX_EIDRM: return "E_POSIX_EIDRM";
+ case E_POSIX_EILSEQ: return "E_POSIX_EILSEQ";
+ case E_POSIX_EINPROGRESS: return "E_POSIX_EINPROGRESS";
+ case E_POSIX_EINTR: return "E_POSIX_EINTR";
+ case E_POSIX_EINVAL: return "E_POSIX_EINVAL";
+ case E_POSIX_EIO: return "E_POSIX_EIO";
+ case E_POSIX_EISCONN: return "E_POSIX_EISCONN";
+ case E_POSIX_EISDIR: return "E_POSIX_EISDIR";
+ case E_POSIX_ELOOP: return "E_POSIX_ELOOP";
+ case E_POSIX_EMFILE: return "E_POSIX_EMFILE";
+ case E_POSIX_EMLINK: return "E_POSIX_EMLINK";
+ case E_POSIX_EMSGSIZE: return "E_POSIX_EMSGSIZE";
+ case E_POSIX_EMULTIHOP: return "E_POSIX_EMULTIHOP";
+ case E_POSIX_ENAMETOOLONG: return "E_POSIX_ENAMETOOLONG";
+ case E_POSIX_ENETDOWN: return "E_POSIX_ENETDOWN";
+ case E_POSIX_ENETRESET: return "E_POSIX_ENETRESET";
+ case E_POSIX_ENETUNREACH: return "E_POSIX_ENETUNREACH";
+ case E_POSIX_ENFILE: return "E_POSIX_ENFILE";
+ case E_POSIX_ENOBUFS: return "E_POSIX_ENOBUFS";
+ case E_POSIX_ENODEV: return "E_POSIX_ENODEV";
+ case E_POSIX_ENOENT: return "E_POSIX_ENOENT";
+ case E_POSIX_ENOEXEC: return "E_POSIX_ENOEXEC";
+ case E_POSIX_ENOLCK: return "E_POSIX_ENOLCK";
+ case E_POSIX_ENOLINK: return "E_POSIX_ENOLINK";
+ case E_POSIX_ENOMEM: return "E_POSIX_ENOMEM";
+ case E_POSIX_ENOMSG: return "E_POSIX_ENOMSG";
+ case E_POSIX_ENOPROTOOPT: return "E_POSIX_ENOPROTOOPT";
+ case E_POSIX_ENOSPC: return "E_POSIX_ENOSPC";
+ case E_POSIX_ENOSYS: return "E_POSIX_ENOSYS";
+ case E_POSIX_ENOTCONN: return "E_POSIX_ENOTCONN";
+ case E_POSIX_ENOTDIR: return "E_POSIX_ENOTDIR";
+ case E_POSIX_ENOTEMPTY: return "E_POSIX_ENOTEMPTY";
+ case E_POSIX_ENOTRECOVERABLE: return "E_POSIX_ENOTRECOVERABLE";
+ case E_POSIX_ENOTSOCK: return "E_POSIX_ENOTSOCK";
+ case E_POSIX_ENOTSUP: return "E_POSIX_ENOTSUP";
+ case E_POSIX_ENOTTY: return "E_POSIX_ENOTTY";
+ case E_POSIX_ENXIO: return "E_POSIX_ENXIO";
+ case E_POSIX_EOPNOTSUPP: return "E_POSIX_EOPNOTSUPP";
+ case E_POSIX_EOVERFLOW: return "E_POSIX_EOVERFLOW";
+ case E_POSIX_EOWNERDEAD: return "E_POSIX_EOWNERDEAD";
+ case E_POSIX_EPERM: return "E_POSIX_EPERM";
+ case E_POSIX_EPIPE: return "E_POSIX_EPIPE";
+ case E_POSIX_EPROTO: return "E_POSIX_EPROTO";
+ case E_POSIX_EPROTONOSUPPORT: return "E_POSIX_EPROTONOSUPPORT";
+ case E_POSIX_EPROTOTYPE: return "E_POSIX_EPROTOTYPE";
+ case E_POSIX_ERANGE: return "E_POSIX_ERANGE";
+ case E_POSIX_EROFS: return "E_POSIX_EROFS";
+ case E_POSIX_ESOCKTNOSUPPORT: return "E_POSIX_ESOCKTNOSUPPORT";
+ case E_POSIX_ESPIPE: return "E_POSIX_ESPIPE";
+ case E_POSIX_ESRCH: return "E_POSIX_ESRCH";
+ case E_POSIX_ESTALE: return "E_POSIX_ESTALE";
+ case E_POSIX_ETIMEDOUT: return "E_POSIX_ETIMEDOUT";
+ case E_POSIX_ETXTBSY: return "E_POSIX_ETXTBSY";
+ case E_POSIX_EWOULDBLOCK: return "E_POSIX_EWOULDBLOCK";
+ case E_POSIX_EXDEV: return "E_POSIX_EXDEV";
+ case E_EUNKNOWN: return "E_EUNKNOWN";
+ default: return "E_<invalid>";
+ }
+}
+
+const char *_errnum_str_msg(_errnum errnum) {
+ switch (errnum) {
+ case E_NOERROR: return "no error";
+ case E_EOF: return "EOF";
+ case E_NET_EARP_TIMEOUT: return "ARP timeout";
+ case E_NET_EACK_TIMEOUT: return "TCP ACK timeout";
+ case E_NET_ERECV_TIMEOUT: return "receive timeout";
+ case E_NET_ECLOSED: return "already closed";
+ case E_POSIX_E2BIG: return "Argument list too long";
+ case E_POSIX_EACCES: return "Permission denied";
+ case E_POSIX_EADDRINUSE: return "Address in use";
+ case E_POSIX_EADDRNOTAVAIL: return "Address not available";
+ case E_POSIX_EAFNOSUPPORT: return "Address family not supported";
+ case E_POSIX_EAGAIN: return "Resource unavailable, try again";
+ case E_POSIX_EALREADY: return "Connection already in progress";
+ case E_POSIX_EBADF: return "Bad file descriptor";
+ case E_POSIX_EBADMSG: return "Bad message";
+ case E_POSIX_EBUSY: return "Device or resource busy";
+ case E_POSIX_ECANCELED: return "Operation canceled";
+ case E_POSIX_ECHILD: return "No child processes";
+ case E_POSIX_ECONNABORTED: return "Connection aborted";
+ case E_POSIX_ECONNREFUSED: return "Connection refused";
+ case E_POSIX_ECONNRESET: return "Connection reset";
+ case E_POSIX_EDEADLK: return "Resource deadlock would occur";
+ case E_POSIX_EDESTADDRREQ: return "Destination address required";
+ case E_POSIX_EDOM: return "Mathematics argument out of domain of function";
+ case E_POSIX_EDQUOT: return "Reserved";
+ case E_POSIX_EEXIST: return "File exists";
+ case E_POSIX_EFAULT: return "Bad address";
+ case E_POSIX_EFBIG: return "File too large";
+ case E_POSIX_EHOSTUNREACH: return "Host is unreachable";
+ case E_POSIX_EIDRM: return "Identifier removed";
+ case E_POSIX_EILSEQ: return "Illegal byte sequence";
+ case E_POSIX_EINPROGRESS: return "Operation in progress";
+ case E_POSIX_EINTR: return "Interrupted function";
+ case E_POSIX_EINVAL: return "Invalid argument";
+ case E_POSIX_EIO: return "I/O error";
+ case E_POSIX_EISCONN: return "Socket is connected";
+ case E_POSIX_EISDIR: return "Is a directory";
+ case E_POSIX_ELOOP: return "Too many levels of symbolic links";
+ case E_POSIX_EMFILE: return "File descriptor value too large";
+ case E_POSIX_EMLINK: return "Too many hard links";
+ case E_POSIX_EMSGSIZE: return "Message too large";
+ case E_POSIX_EMULTIHOP: return "Reserved";
+ case E_POSIX_ENAMETOOLONG: return "Filename too long";
+ case E_POSIX_ENETDOWN: return "Network is down";
+ case E_POSIX_ENETRESET: return "Connection aborted by network";
+ case E_POSIX_ENETUNREACH: return "Network unreachable";
+ case E_POSIX_ENFILE: return "Too many files open in system";
+ case E_POSIX_ENOBUFS: return "No buffer space available";
+ case E_POSIX_ENODEV: return "No such device";
+ case E_POSIX_ENOENT: return "No such file or directory";
+ case E_POSIX_ENOEXEC: return "Executable file format error";
+ case E_POSIX_ENOLCK: return "No locks available";
+ case E_POSIX_ENOLINK: return "Reserved";
+ case E_POSIX_ENOMEM: return "Not enough space";
+ case E_POSIX_ENOMSG: return "No message of the desired type";
+ case E_POSIX_ENOPROTOOPT: return "Protocol not available";
+ case E_POSIX_ENOSPC: return "No space left on device";
+ case E_POSIX_ENOSYS: return "Functionality not supported";
+ case E_POSIX_ENOTCONN: return "The socket is not connected";
+ case E_POSIX_ENOTDIR: return "Not a directory or a symbolic link to a directory";
+ case E_POSIX_ENOTEMPTY: return "Directory not empty";
+ case E_POSIX_ENOTRECOVERABLE: return "State not recoverable";
+ case E_POSIX_ENOTSOCK: return "Not a socket";
+ case E_POSIX_ENOTSUP: return "Not supported";
+ case E_POSIX_ENOTTY: return "Inappropriate I/O control operation";
+ case E_POSIX_ENXIO: return "No such device or address";
+ case E_POSIX_EOPNOTSUPP: return "Operation not supported on socket";
+ case E_POSIX_EOVERFLOW: return "Value too large to be stored in data type";
+ case E_POSIX_EOWNERDEAD: return "Previous owner died";
+ case E_POSIX_EPERM: return "Operation not permitted";
+ case E_POSIX_EPIPE: return "Broken pipe";
+ case E_POSIX_EPROTO: return "Protocol error";
+ case E_POSIX_EPROTONOSUPPORT: return "Protocol not supported";
+ case E_POSIX_EPROTOTYPE: return "Protocol wrong type for socket";
+ case E_POSIX_ERANGE: return "Result too large";
+ case E_POSIX_EROFS: return "Read-only file system";
+ case E_POSIX_ESOCKTNOSUPPORT: return "Socket type not supported";
+ case E_POSIX_ESPIPE: return "Invalid seek";
+ case E_POSIX_ESRCH: return "No such process";
+ case E_POSIX_ESTALE: return "Reserved";
+ case E_POSIX_ETIMEDOUT: return "Connection timed out";
+ case E_POSIX_ETXTBSY: return "Text file busy";
+ case E_POSIX_EWOULDBLOCK: return "Operation would block";
+ case E_POSIX_EXDEV: return "Improper hard link";
+ case E_EUNKNOWN:
+ default:
+ return "unknown error";
+ }
+}
diff --git a/libmisc/error_generated.c.gen b/libmisc/error_generated.c.gen
new file mode 100755
index 0000000..944d909
--- /dev/null
+++ b/libmisc/error_generated.c.gen
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# libmisc/error_generated.c.gen - Generate _errnum strings
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+error_h=$1
+outfile=$2
+
+{
+ echo "/* ${outfile} - Generated by $0. DO NOT EDIT! */"
+ echo
+ echo '#include <libmisc/error.h>'
+ echo
+ echo 'const char *_errnum_str_sym(_errnum errnum) {'
+ echo $'\tswitch (errnum) {'
+ sed -nE \
+ -e 's@^(#define)?\s+(E_[_A-Z0-9]+)[ ,][^/]*/\* ([^*(]+) (\*/|\().*@'$'\tcase \\2: return "\\2";''@p' \
+ -- "$error_h"
+ echo $'\tcase E_EUNKNOWN: return "E_EUNKNOWN";'
+ echo $'\tdefault: return "E_<invalid>";'
+ echo $'\t}'
+ echo '}'
+ echo
+ echo 'const char *_errnum_str_msg(_errnum errnum) {'
+ echo $'\tswitch (errnum) {'
+ sed -nE \
+ -e 's@^(#define)?\s+(E_[_A-Z0-9]+)[ ,][^/]*/\* ([^*(]+) (\*/|\().*@'$'\tcase \\2: return "\\3";''@p' \
+ -- "$error_h"
+ echo $'\tcase E_EUNKNOWN:'
+ echo $'\tdefault:'
+ echo $'\t\treturn "unknown error";'
+ echo $'\t}'
+ echo '}'
+} >"$outfile"
diff --git a/libmisc/fmt.c b/libmisc/fmt.c
new file mode 100644
index 0000000..7c18ef5
--- /dev/null
+++ b/libmisc/fmt.c
@@ -0,0 +1,266 @@
+/* libmisc/fmt.c - Write formatted text
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <string.h> /* for strnlen() */
+
+#include <libmisc/utf8.h>
+
+#include <libmisc/fmt.h>
+
+static const char *const hexdig = "0123456789ABCDEF";
+
+/* small/trivial formatters ***************************************************/
+
+void fmt_print_mem(lo_interface fmt_dest w, const void *_str, size_t size) {
+ const uint8_t *str = _str;
+ while (size--)
+ fmt_print_byte(w, *(str++));
+}
+void fmt_print_str(lo_interface fmt_dest w, const char *str) {
+ while (*str)
+ fmt_print_byte(w, *(str++));
+}
+void fmt_print_strn(lo_interface fmt_dest w, const char *str, size_t size) {
+ while (size-- && *str)
+ fmt_print_byte(w, *(str++));
+}
+
+void fmt_print_hmem(lo_interface fmt_dest w, const void *_str, size_t size) {
+ const uint8_t *str = _str;
+ fmt_print_byte(w, '{');
+ for (size_t i = 0; i < size; i++) {
+ if (i)
+ fmt_print_byte(w, ',');
+ fmt_print_hbyte(w, str[i]);
+ }
+ fmt_print_byte(w, '}');
+}
+
+void fmt_print_byte(lo_interface fmt_dest w, uint8_t b) {
+ LO_CALL(w, putb, b);
+}
+
+void fmt_print_bool(lo_interface fmt_dest w, bool b) {
+ fmt_print_str(w, b ? "true" : "false");
+}
+
+void fmt_print_base16_u8_(lo_interface fmt_dest w, uint8_t x) {
+ fmt_print(w, "0x", (rjust, 2, '0', (base16, x)));
+}
+void fmt_print_base16_u16_(lo_interface fmt_dest w, uint16_t x) {
+ fmt_print(w, "0x", (rjust, 4, '0', (base16, x)));
+}
+void fmt_print_base16_u32_(lo_interface fmt_dest w, uint32_t x) {
+ fmt_print(w, "0x", (rjust, 8, '0', (base16, x)));
+}
+void fmt_print_base16_u64_(lo_interface fmt_dest w, uint64_t x) {
+ fmt_print(w, "0x", (rjust, 16, '0', (base16, x)));
+}
+
+void fmt_print_ptr(lo_interface fmt_dest w, void *ptr) {
+ LM_CAT3_(fmt_print_base16_u, __INTPTR_WIDTH__, _)(w, (uintptr_t)ptr);
+}
+
+/* quote **********************************************************************/
+
+/**
+ * Quote a byte to ASCII-only C syntax.
+ */
+void fmt_print_qbyte(lo_interface fmt_dest w, uint8_t b) {
+ fmt_print_byte(w, '\'');
+ if (b == '\0' ||
+ b == '\b' ||
+ b == '\f' ||
+ b == '\n' ||
+ b == '\r' ||
+ b == '\t' ||
+ b == '\v' ||
+ b == '\\' ||
+ b == '\'' ||
+ b == '"' ||
+ b == '?') {
+ fmt_print_byte(w, '\\');
+ switch (b) {
+ case '\0': fmt_print_byte(w, '0'); break;
+ case '\a': fmt_print_byte(w, 'a'); break;
+ case '\b': fmt_print_byte(w, 'b'); break;
+ case '\f': fmt_print_byte(w, 'f'); break;
+ case '\n': fmt_print_byte(w, 'n'); break;
+ case '\r': fmt_print_byte(w, 'r'); break;
+ case '\t': fmt_print_byte(w, 't'); break;
+ case '\v': fmt_print_byte(w, 'v'); break;
+ case '\\': fmt_print_byte(w, '\\'); break;
+ case '\'': fmt_print_byte(w, '\''); break;
+ case '"': fmt_print_byte(w, '"'); break;
+ case '?': fmt_print_byte(w, '?'); break;
+ }
+ } else if (' ' <= b && b <= '~') {
+ fmt_print_byte(w, b);
+ } else {
+ fmt_print_byte(w, '\\');
+ fmt_print_byte(w, 'x');
+ fmt_print_byte(w, hexdig[(b >> 4) & 0xF]);
+ fmt_print_byte(w, hexdig[(b >> 0) & 0xF]);
+ }
+ fmt_print_byte(w, '\'');
+}
+
+/**
+ * Quote a region of memory to ASCII-only C string syntax. Valid
+ * UTF-8 is quoted as short C-escape characters, \uABCD, or
+ * \UABCDABCD; invalid UTF-8 is quoted as \xAB.
+ */
+void fmt_print_qmem(lo_interface fmt_dest w, const void *_str, size_t size) {
+ const uint8_t *str = _str;
+ fmt_print_byte(w, '"');
+ for (size_t pos = 0; pos < size;) {
+ uint32_t ch;
+ uint8_t chlen;
+ utf8_decode_codepoint(&str[pos], size-pos, &ch, &chlen);
+ if (!chlen) {
+ /* invalid UTF-8 */
+ /* \xAB */
+ fmt_print_byte(w, '\\');
+ fmt_print_byte(w, 'x');
+ fmt_print_byte(w, hexdig[(str[pos] >> 4) & 0xF]);
+ fmt_print_byte(w, hexdig[(str[pos] >> 0) & 0xF]);
+ pos++;
+ continue;
+ }
+ if (ch == '\0' ||
+ ch == '\b' ||
+ ch == '\f' ||
+ ch == '\n' ||
+ ch == '\r' ||
+ ch == '\t' ||
+ ch == '\v' ||
+ ch == '\\' ||
+ ch == '\'' ||
+ ch == '"' ||
+ ch == '?') {
+ /* short C-escape */
+ fmt_print_byte(w, '\\');
+ switch (ch) {
+ case '\0': fmt_print_byte(w, '0'); break;
+ case '\a': fmt_print_byte(w, 'a'); break;
+ case '\b': fmt_print_byte(w, 'b'); break;
+ case '\f': fmt_print_byte(w, 'f'); break;
+ case '\n': fmt_print_byte(w, 'n'); break;
+ case '\r': fmt_print_byte(w, 'r'); break;
+ case '\t': fmt_print_byte(w, 't'); break;
+ case '\v': fmt_print_byte(w, 'v'); break;
+ case '\\': fmt_print_byte(w, '\\'); break;
+ case '\'': fmt_print_byte(w, '\''); break;
+ case '"': fmt_print_byte(w, '"'); break;
+ case '?': fmt_print_byte(w, '?'); break;
+ }
+ } else if (' ' <= ch && ch <= '~') {
+ /* no escaping */
+ fmt_print_byte(w, ch);
+ } else if (ch < 0x10000) {
+ /* \uABCD */
+ fmt_print_byte(w, '\\');
+ fmt_print_byte(w, 'u');
+ fmt_print_byte(w, hexdig[(ch >> 12) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 8) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 4) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 0) & 0xF]);
+ } else {
+ /* \UABCDABCD */
+ fmt_print_byte(w, '\\');
+ fmt_print_byte(w, 'U');
+ fmt_print_byte(w, hexdig[(ch >> 28) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 24) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 20) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 16) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 12) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 8) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 4) & 0xF]);
+ fmt_print_byte(w, hexdig[(ch >> 0) & 0xF]);
+ }
+ pos += chlen;
+ }
+ fmt_print_byte(w, '"');
+}
+
+void fmt_print_qstr(lo_interface fmt_dest w, const char *str) {
+ fmt_print_qmem(w, str, strlen(str));
+}
+
+void fmt_print_qstrn(lo_interface fmt_dest w, const char *str, size_t n) {
+ fmt_print_qmem(w, str, strnlen(str, n));
+}
+
+/* int ************************************************************************/
+
+#define declare(BASE, BITS) \
+ void _fmt_print_base##BASE##_s##BITS(lo_interface fmt_dest w, \
+ int##BITS##_t val) { \
+ if (val < 0) { \
+ fmt_print_byte(w, '-'); \
+ val = -val; \
+ } \
+ _fmt_print_base##BASE##_u##BITS(w, (uint##BITS##_t)val); \
+ } \
+ \
+ void _fmt_print_base##BASE##_u##BITS(lo_interface fmt_dest w, \
+ uint##BITS##_t absval) { \
+ /* This digit-counting is O(log(absval)); there are \
+ * `__builtin_clz`-based O(1) ways to do this, but when I \
+ * tried them they bloated the code-size too much. And this \
+ * function as a whole is already O(log(absval)) anyway \
+ * because of actually printing the digits. */ \
+ unsigned ndigits = 1; \
+ uint##BITS##_t div = 1; \
+ while (absval / div >= BASE) { \
+ div *= BASE; \
+ ndigits++; \
+ } \
+ \
+ for (unsigned i = 0; i < ndigits; i++) { \
+ unsigned digit = (unsigned) (absval / div); \
+ absval %= div; \
+ div /= BASE; \
+ fmt_print_byte(w, hexdig[digit]); \
+ } \
+ } \
+ LM_FORCE_SEMICOLON
+
+declare(2, 8);
+declare(2, 16);
+declare(2, 32);
+declare(2, 64);
+
+declare(8, 8);
+declare(8, 16);
+declare(8, 32);
+declare(8, 64);
+
+declare(10, 8);
+declare(10, 16);
+declare(10, 32);
+declare(10, 64);
+
+declare(16, 8);
+declare(16, 16);
+declare(16, 32);
+declare(16, 64);
+
+#undef declare
+
+/* fmt_buf ********************************************************************/
+
+LO_IMPLEMENTATION_C(fmt_dest, struct fmt_buf, fmt_buf, static);
+
+static void fmt_buf_putb(struct fmt_buf *buf, uint8_t b) {
+ if (buf->len < buf->cap)
+ ((uint8_t *)(buf->dat))[buf->len] = b;
+ buf->len++;
+}
+
+static size_t fmt_buf_tell(struct fmt_buf *buf) {
+ return buf->len;
+}
diff --git a/libmisc/hash.c b/libmisc/hash.c
new file mode 100644
index 0000000..3814cec
--- /dev/null
+++ b/libmisc/hash.c
@@ -0,0 +1,24 @@
+/* libmisc/hash.c - General-purpose hash utilities
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/hash.h>
+
+/* djb2 hash */
+void hash_init(hash_t *hash) {
+ *hash = 5381;
+}
+void hash_write(hash_t *hash, void *dat, size_t len) {
+ for (size_t i = 0; i < len; i++)
+ *hash = (*hash * 33) + (hash_t)(((unsigned char *)dat)[i]);
+}
+
+/* utilities */
+hash_t hash(void *dat, size_t len) {
+ hash_t h;
+ hash_init(&h);
+ hash_write(&h, dat, len);
+ return h;
+}
diff --git a/libmisc/include/libmisc/_intercept.h b/libmisc/include/libmisc/_intercept.h
new file mode 100644
index 0000000..fa327d6
--- /dev/null
+++ b/libmisc/include/libmisc/_intercept.h
@@ -0,0 +1,24 @@
+/* libmisc/_intercept.h - Interceptable ("weak") functions
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC__INTERCEPT_H_
+#define _LIBMISC__INTERCEPT_H_
+
+/* pico-sdk/newlib defines abort() to be [[gnu::weak]] already, but
+ * depending on optimization options glibc might not, and GCC might
+ * assume it knows what it does and optimize it out. So define our
+ * own `__lm_` wrapper that GCC/glibc won't interfere with.
+ */
+
+[[noreturn]] void __lm_abort(void);
+
+/* While newlib defines putchar() to be [[gnu::weak]], pico_stdio does
+ * not. Plus the above about optimizations.
+ */
+
+void __lm_putchar(unsigned char c);
+
+#endif /* _LIBMISC__INTERCEPT_H_ */
diff --git a/libmisc/include/libmisc/alloc.h b/libmisc/include/libmisc/alloc.h
new file mode 100644
index 0000000..afddbce
--- /dev/null
+++ b/libmisc/include/libmisc/alloc.h
@@ -0,0 +1,26 @@
+/* libmisc/alloc.h - Type-safe wrappers around alloca and malloc
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_ALLOC_H_
+#define _LIBMISC_ALLOC_H_
+
+#include <alloca.h> /* for alloca() */
+#include <stdlib.h> /* for calloc(), free() */
+#include <string.h> /* for memset() */
+
+#define stack_alloc(N, TYP) ({ \
+ size_t _size; \
+ TYP *_ret = NULL; \
+ if (!__builtin_mul_overflow(N, sizeof(TYP), &_size)) { \
+ _ret = alloca(_size); \
+ memset(_ret, 0, _size); \
+ } \
+ _ret; \
+})
+
+#define heap_alloc(N, TYP) ((TYP *)calloc(N, sizeof(TYP)))
+
+#endif /* _LIBMISC_ALLOC_H_ */
diff --git a/libmisc/include/libmisc/assert.h b/libmisc/include/libmisc/assert.h
index da2ba2b..ccdb288 100644
--- a/libmisc/include/libmisc/assert.h
+++ b/libmisc/include/libmisc/assert.h
@@ -1,6 +1,6 @@
/* libmisc/assert.h - More assertions
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -10,15 +10,20 @@
#ifdef NDEBUG
# define __assert_msg(expr, expr_str, msg) ((void)0)
#else
-# define __assert_msg(expr, expr_str, msg) do { if (!(expr)) __assert_msg_fail(expr_str, __FILE__, __LINE__, __func__, msg); } while (0)
+# define __assert_msg(expr, expr_str, msg) \
+ do { \
+ if (!(expr)) \
+ __assert_msg_fail(expr_str, __FILE__, __LINE__, __func__, msg); \
+ } while (0)
[[noreturn]] void __assert_msg_fail(const char *expr,
const char *file, unsigned int line, const char *func,
const char *msg);
#endif
-#define assert_msg(expr, msg) __assert_msg(expr, #expr, msg) /* libmisc */
-#define assert(expr) __assert_msg(expr, #expr, NULL) /* C89, POSIX-2001 */
-#define assert_notreached(msg) do { __assert_msg(0, "notreached", msg); __builtin_unreachable(); } while (0) /* libmisc */
-#define static_assert _Static_assert /* C11 */
+#define assert_msg(expr, msg) __assert_msg(expr, #expr, msg) /* libmisc */
+#define assert(expr) __assert_msg(expr, #expr, 0) /* C89, POSIX-2001 */
+#define assert_notreached(msg) do { __assert_msg(0, "notreached", msg); __builtin_unreachable(); } while (0) /* libmisc */
+#define static_assert _Static_assert /* C11 */
+#define static_assert_as_expr(...) (sizeof(struct {static_assert(__VA_ARGS__);})) /* libmisc */
#endif /* _LIBMISC_ASSERT_H_ */
diff --git a/libmisc/include/libmisc/endian.h b/libmisc/include/libmisc/endian.h
index b1bc55c..966c3bc 100644
--- a/libmisc/include/libmisc/endian.h
+++ b/libmisc/include/libmisc/endian.h
@@ -1,134 +1,34 @@
/* libmisc/endian.h - Endian-conversion helpers
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _LIBMISC_ENDIAN_H_
#define _LIBMISC_ENDIAN_H_
+#include <stddef.h> /* for size_t */
#include <stdint.h> /* for uint{n}_t */
-#include <libmisc/assert.h>
-
-/* Big endian *****************************************************************/
-
-typedef struct {
- uint8_t octets[2];
-} uint16be_t;
-static_assert(sizeof(uint16be_t) == 2);
-
-static inline size_t uint16be_encode(uint8_t *out, uint16_t in) {
- out[0] = (uint8_t)((in >> 8) & 0xFF);
- out[1] = (uint8_t)((in >> 0) & 0xFF);
- return 2;
-}
-
-static inline uint16_t uint16be_decode(uint8_t *in) {
- return (((uint16_t)(in[0])) << 8)
- | (((uint16_t)(in[1])) << 0)
- ;
-}
-
-static inline uint16be_t uint16be_marshal(uint16_t in) {
- uint16be_t out;
- uint16be_encode(out.octets, in);
- return out;
-}
-
-static inline uint16_t uint16be_unmarshal(uint16be_t in) {
- return uint16be_decode(in.octets);
-}
-
-typedef struct {
- uint8_t octets[4];
-} uint32be_t;
-static_assert(sizeof(uint32be_t) == 4);
-
-static inline size_t uint32be_encode(uint8_t *out, uint32_t in) {
- out[0] = (uint8_t)((in >> 24) & 0xFF);
- out[1] = (uint8_t)((in >> 16) & 0xFF);
- out[2] = (uint8_t)((in >> 8) & 0xFF);
- out[3] = (uint8_t)((in >> 0) & 0xFF);
- return 4;
-}
-
-static inline uint32_t uint32be_decode(uint8_t *in) {
- return (((uint32_t)(in[0])) << 24)
- | (((uint32_t)(in[1])) << 16)
- | (((uint32_t)(in[2])) << 8)
- | (((uint32_t)(in[3])) << 0)
- ;
-}
-
-static inline uint32be_t uint32be_marshal(uint32_t in) {
- uint32be_t out;
- uint32be_encode(out.octets, in);
- return out;
-}
-
-static inline uint32_t uint32be_unmarshal(uint32be_t in) {
- return uint32be_decode(in.octets);
-}
-
-/* Little endian **************************************************************/
-
-typedef struct {
- uint8_t octets[2];
-} uint16le_t;
-static_assert(sizeof(uint16le_t) == 2);
-
-static inline size_t uint16le_encode(uint8_t *out, uint16_t in) {
- out[0] = (uint8_t)((in >> 0) & 0xFF);
- out[1] = (uint8_t)((in >> 8) & 0xFF);
- return 2;
-}
-
-static inline uint16_t uint16le_decode(uint8_t *in) {
- return (((uint16_t)(in[0])) << 0)
- | (((uint16_t)(in[1])) << 8)
- ;
-}
-
-static inline uint16le_t uint16le_marshal(uint16_t in) {
- uint16le_t out;
- uint16le_encode(out.octets, in);
- return out;
-}
-
-static inline uint16_t uint16le_unmarshal(uint16le_t in) {
- return uint16le_decode(in.octets);
-}
-
-typedef struct {
- uint8_t octets[4];
-} uint32le_t;
-static_assert(sizeof(uint32le_t) == 4);
-
-static inline size_t uint32le_encode(uint8_t *out, uint32_t in) {
- out[0] = (uint8_t)((in >> 0) & 0xFF);
- out[1] = (uint8_t)((in >> 8) & 0xFF);
- out[2] = (uint8_t)((in >> 16) & 0xFF);
- out[3] = (uint8_t)((in >> 24) & 0xFF);
- return 4;
-}
-
-static inline uint32_t uint32le_decode(uint8_t *in) {
- return (((uint32_t)(in[0])) << 0)
- | (((uint32_t)(in[1])) << 8)
- | (((uint32_t)(in[2])) << 16)
- | (((uint32_t)(in[3])) << 24)
- ;
-}
-
-static inline uint32le_t uint32le_marshal(uint32_t in) {
- uint32le_t out;
- uint32le_encode(out.octets, in);
- return out;
-}
-
-static inline uint32_t uint32le_unmarshal(uint32le_t in) {
- return uint32le_decode(in.octets);
-}
+#define _endian_declare_conv(NBIT, ENDIAN) \
+ /* byte array encode/decode */ \
+ size_t uint##NBIT##ENDIAN##_encode(uint8_t *out, uint##NBIT##_t in); \
+ uint##NBIT##_t uint##NBIT##ENDIAN##_decode(uint8_t *in); \
+ /* struct marshal/unmarshal */ \
+ typedef struct { \
+ uint8_t octets[NBIT/8]; \
+ } uint##NBIT##ENDIAN##_t; \
+ uint##NBIT##ENDIAN##_t uint##NBIT##ENDIAN##_marshal(uint##NBIT##_t in); \
+ uint##NBIT##_t uint##NBIT##ENDIAN##_unmarshal(uint##NBIT##ENDIAN##_t in)
+
+_endian_declare_conv(16, be);
+_endian_declare_conv(32, be);
+_endian_declare_conv(64, be);
+
+_endian_declare_conv(16, le);
+_endian_declare_conv(32, le);
+_endian_declare_conv(64, le);
+
+#undef _endian_declare_conv
#endif /* _LIBMISC_ENDIAN_H_ */
diff --git a/libmisc/include/libmisc/error.h b/libmisc/include/libmisc/error.h
new file mode 100644
index 0000000..4110626
--- /dev/null
+++ b/libmisc/include/libmisc/error.h
@@ -0,0 +1,167 @@
+/* libmisc/error.h - Go-esque errors
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_ERROR_H_
+#define _LIBMISC_ERROR_H_
+
+#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for uint{n}_t */
+
+#include <libmisc/assert.h>
+#include <libmisc/fmt.h>
+
+/* _errnum ********************************************************************/
+
+typedef enum {
+ /* Original to libmisc */
+ E_NOERROR = 0, /* no error */
+ E_EOF, /* EOF */
+ E_NET_EARP_TIMEOUT, /* ARP timeout */
+ E_NET_EACK_TIMEOUT, /* TCP ACK timeout */
+ E_NET_ERECV_TIMEOUT, /* receive timeout */
+ E_NET_ECLOSED, /* already closed */
+ /* At least C99 through C23 */
+ E_STDC_EDOM,
+ E_STDC_EILSEQ,
+ E_STDC_ERANGE,
+ /* POSIX-2024 */
+ E_POSIX_E2BIG, /* Argument list too long */
+ E_POSIX_EACCES, /* Permission denied */
+ E_POSIX_EADDRINUSE, /* Address in use */
+ E_POSIX_EADDRNOTAVAIL, /* Address not available */
+ E_POSIX_EAFNOSUPPORT, /* Address family not supported */
+ E_POSIX_EAGAIN, /* Resource unavailable, try again (may be the same value as [EWOULDBLOCK]) */
+ E_POSIX_EALREADY, /* Connection already in progress */
+ E_POSIX_EBADF, /* Bad file descriptor */
+ E_POSIX_EBADMSG, /* Bad message */
+ E_POSIX_EBUSY, /* Device or resource busy */
+ E_POSIX_ECANCELED, /* Operation canceled */
+ E_POSIX_ECHILD, /* No child processes */
+ E_POSIX_ECONNABORTED, /* Connection aborted */
+ E_POSIX_ECONNREFUSED, /* Connection refused */
+ E_POSIX_ECONNRESET, /* Connection reset */
+ E_POSIX_EDEADLK, /* Resource deadlock would occur */
+ E_POSIX_EDESTADDRREQ, /* Destination address required */
+#define E_POSIX_EDOM E_STDC_EDOM /* Mathematics argument out of domain of function */
+ E_POSIX_EDQUOT, /* Reserved */
+ E_POSIX_EEXIST, /* File exists */
+ E_POSIX_EFAULT, /* Bad address */
+ E_POSIX_EFBIG, /* File too large */
+ E_POSIX_EHOSTUNREACH, /* Host is unreachable */
+ E_POSIX_EIDRM, /* Identifier removed */
+#define E_POSIX_EILSEQ E_STDC_EILSEQ /* Illegal byte sequence */ /* (in the context of text encoding) */
+ E_POSIX_EINPROGRESS, /* Operation in progress */
+ E_POSIX_EINTR, /* Interrupted function */
+ E_POSIX_EINVAL, /* Invalid argument */
+ E_POSIX_EIO, /* I/O error */
+ E_POSIX_EISCONN, /* Socket is connected */
+ E_POSIX_EISDIR, /* Is a directory */
+ E_POSIX_ELOOP, /* Too many levels of symbolic links */
+ E_POSIX_EMFILE, /* File descriptor value too large */
+ E_POSIX_EMLINK, /* Too many hard links */
+ E_POSIX_EMSGSIZE, /* Message too large */
+ E_POSIX_EMULTIHOP, /* Reserved */
+ E_POSIX_ENAMETOOLONG, /* Filename too long */
+ E_POSIX_ENETDOWN, /* Network is down */
+ E_POSIX_ENETRESET, /* Connection aborted by network */
+ E_POSIX_ENETUNREACH, /* Network unreachable */
+ E_POSIX_ENFILE, /* Too many files open in system */
+ E_POSIX_ENOBUFS, /* No buffer space available */
+ E_POSIX_ENODEV, /* No such device */
+ E_POSIX_ENOENT, /* No such file or directory */
+ E_POSIX_ENOEXEC, /* Executable file format error */
+ E_POSIX_ENOLCK, /* No locks available */
+ E_POSIX_ENOLINK, /* Reserved */
+ E_POSIX_ENOMEM, /* Not enough space */
+ E_POSIX_ENOMSG, /* No message of the desired type */
+ E_POSIX_ENOPROTOOPT, /* Protocol not available */
+ E_POSIX_ENOSPC, /* No space left on device */
+ E_POSIX_ENOSYS, /* Functionality not supported */
+ E_POSIX_ENOTCONN, /* The socket is not connected */
+ E_POSIX_ENOTDIR, /* Not a directory or a symbolic link to a directory */
+ E_POSIX_ENOTEMPTY, /* Directory not empty */
+ E_POSIX_ENOTRECOVERABLE, /* State not recoverable */ /* Added in POSIX-2008 */
+ E_POSIX_ENOTSOCK, /* Not a socket */
+ E_POSIX_ENOTSUP, /* Not supported (may be the same value as [EOPNOTSUPP]) */
+ E_POSIX_ENOTTY, /* Inappropriate I/O control operation */
+ E_POSIX_ENXIO, /* No such device or address */
+ E_POSIX_EOPNOTSUPP, /* Operation not supported on socket (may be the same value as [ENOTSUP]) */
+ E_POSIX_EOVERFLOW, /* Value too large to be stored in data type */
+ E_POSIX_EOWNERDEAD, /* Previous owner died */ /* Added in POSIX-2008 */
+ E_POSIX_EPERM, /* Operation not permitted */
+ E_POSIX_EPIPE, /* Broken pipe */
+ E_POSIX_EPROTO, /* Protocol error */
+ E_POSIX_EPROTONOSUPPORT, /* Protocol not supported */
+ E_POSIX_EPROTOTYPE, /* Protocol wrong type for socket */
+#define E_POSIX_ERANGE E_STDC_ERANGE /* Result too large */
+ E_POSIX_EROFS, /* Read-only file system */
+ E_POSIX_ESOCKTNOSUPPORT, /* Socket type not supported */
+ E_POSIX_ESPIPE, /* Invalid seek */
+ E_POSIX_ESRCH, /* No such process */
+ E_POSIX_ESTALE, /* Reserved */
+ E_POSIX_ETIMEDOUT, /* Connection timed out */
+ E_POSIX_ETXTBSY, /* Text file busy */
+ E_POSIX_EWOULDBLOCK, /* Operation would block (may be the same value as [EAGAIN]) */
+ E_POSIX_EXDEV, /* Improper hard link */
+ /* End cap */
+ E_EUNKNOWN,
+} _errnum;
+
+const char *_errnum_str_sym(_errnum);
+const char *_errnum_str_msg(_errnum);
+
+/* error **********************************************************************/
+
+typedef struct {
+ _errnum num;
+ char *_msg;
+} error;
+
+#ifdef NDEBUG
+#define error_new(ERRNUM, ...) ((error){ \
+ .num = ERRNUM , \
+ __VA_OPT__(._msg = fmt_asprint(__VA_ARGS__),) \
+})
+#else
+#define error_new(ERRNUM, ...) ((error){ \
+ .num = ERRNUM, \
+ ._msg = fmt_asprint("" __VA_OPT__(,) __VA_ARGS__), \
+})
+#endif
+
+#define ERROR_NULL ((error){})
+#define ERROR_IS_NULL(err) ((err).num == 0 && (err)._msg == NULL)
+
+const char *error_msg(error err);
+void error_cleanup(error *errptr);
+void fmt_print_error(lo_interface fmt_dest w, error err);
+
+/* or_error ******************************************************************/
+
+#define DECLARE_ERROR_OR(TYP) typedef struct { union { TYP TYP; error err; }; bool is_err; } TYP##_or_error
+#define ERROR_NEW_VAL(TYP, VAL) ((TYP##_or_error){ .TYP = (VAL), .is_err = false })
+#define ERROR_NEW_ERR(TYP, ERR) ((TYP##_or_error){ .err = (ERR), .is_err = true })
+
+/* and_error *****************************************************************/
+
+#define DECLARE_ERROR_AND(TYP) typedef struct { TYP TYP; error err; } TYP##_and_error
+#define ERROR_AND(TYP, VAL, ERR) ((TYP##_and_error){ .TYP = (VAL), .err = (ERR) })
+
+/* some pre-defined types ****************************************************/
+
+DECLARE_ERROR_OR(size_t);
+DECLARE_ERROR_OR(uint8_t);
+DECLARE_ERROR_OR(uint16_t);
+DECLARE_ERROR_OR(uint32_t);
+DECLARE_ERROR_OR(uint64_t);
+
+DECLARE_ERROR_AND(size_t);
+DECLARE_ERROR_AND(uint8_t);
+DECLARE_ERROR_AND(uint16_t);
+DECLARE_ERROR_AND(uint32_t);
+DECLARE_ERROR_AND(uint64_t);
+
+#endif /* _LIBMISC_ERROR_H_ */
diff --git a/libmisc/include/libmisc/fmt.h b/libmisc/include/libmisc/fmt.h
new file mode 100644
index 0000000..135e48b
--- /dev/null
+++ b/libmisc/include/libmisc/fmt.h
@@ -0,0 +1,163 @@
+/* libmisc/fmt.h - Write formatted text
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_FMT_H_
+#define _LIBMISC_FMT_H_
+
+#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for (u)int{n}_t */
+#include <stdlib.h> /* for realloc() */
+
+#include <libmisc/macro.h>
+#include <libmisc/obj.h>
+
+/* destination interface ******************************************************/
+
+#define fmt_dest_LO_IFACE \
+ LO_FUNC(void , putb, uint8_t b) \
+ LO_FUNC(size_t, tell)
+LO_INTERFACE(fmt_dest);
+
+/* type-specific fmt_print_() functions ***************************************/
+
+/* Simple bytes. */
+void fmt_print_byte(lo_interface fmt_dest w, uint8_t b);
+void fmt_print_mem(lo_interface fmt_dest w, const void *str, size_t size);
+void fmt_print_str(lo_interface fmt_dest w, const char *str);
+void fmt_print_strn(lo_interface fmt_dest w, const char *str, size_t size);
+
+/* Quoted bytes. */
+void fmt_print_qbyte(lo_interface fmt_dest w, uint8_t b);
+void fmt_print_qmem(lo_interface fmt_dest w, const void *str, size_t size);
+void fmt_print_qstr(lo_interface fmt_dest w, const char *str);
+void fmt_print_qstrn(lo_interface fmt_dest w, const char *str, size_t size);
+
+/* Hex bytes. */
+#define fmt_print_hbyte fmt_print_base16_u8_
+void fmt_print_hmem(lo_interface fmt_dest w, const void *str, size_t size);
+
+/* Integers. */
+#define _fmt_declare_base(base) \
+ void _fmt_print_base##base##_u8(lo_interface fmt_dest w, uint8_t val); \
+ void _fmt_print_base##base##_u16(lo_interface fmt_dest w, uint16_t val); \
+ void _fmt_print_base##base##_u32(lo_interface fmt_dest w, uint32_t val); \
+ void _fmt_print_base##base##_u64(lo_interface fmt_dest w, uint64_t val); \
+ void _fmt_print_base##base##_s8(lo_interface fmt_dest w, int8_t val); \
+ void _fmt_print_base##base##_s16(lo_interface fmt_dest w, int16_t val); \
+ void _fmt_print_base##base##_s32(lo_interface fmt_dest w, int32_t val); \
+ void _fmt_print_base##base##_s64(lo_interface fmt_dest w, int64_t val); \
+ LM_FORCE_SEMICOLON
+_fmt_declare_base(2);
+_fmt_declare_base(8);
+_fmt_declare_base(10);
+_fmt_declare_base(16);
+#undef _fmt_declare_base
+
+#define _fmt_intswitch(val, prefix) _Generic((val) , \
+ unsigned char : LM_CAT3_(prefix, u, __SCHAR_WIDTH__) , \
+ unsigned short : LM_CAT3_(prefix, u, __SHRT_WIDTH__) , \
+ unsigned int : LM_CAT3_(prefix, u, __INT_WIDTH__) , \
+ unsigned long : LM_CAT3_(prefix, u, __LONG_WIDTH__) , \
+ unsigned long long : LM_CAT3_(prefix, u, __LONG_LONG_WIDTH__) , \
+ signed char : LM_CAT3_(prefix, s, __SCHAR_WIDTH__) , \
+ signed short : LM_CAT3_(prefix, s, __SHRT_WIDTH__) , \
+ signed int : LM_CAT3_(prefix, s, __INT_WIDTH__) , \
+ signed long : LM_CAT3_(prefix, s, __LONG_WIDTH__) , \
+ signed long long : LM_CAT3_(prefix, s, __LONG_LONG_WIDTH__) )
+#define fmt_print_base2(w, val) (_fmt_intswitch((val), _fmt_print_base2_)((w), (val)))
+#define fmt_print_base8(w, val) (_fmt_intswitch((val), _fmt_print_base8_)((w), (val)))
+#define fmt_print_base10(w, val) (_fmt_intswitch((val), _fmt_print_base10_)((w), (val)))
+#define fmt_print_base16(w, val) (_fmt_intswitch((val), _fmt_print_base16_)((w), (val)))
+
+/* Booleans. */
+void fmt_print_bool(lo_interface fmt_dest w, bool b);
+
+/* The high-level fmt_print() interface ***************************************/
+
+#define fmt_print(w, ...) do { LM_FOREACH_PARAM_(_fmt_param, (w), __VA_ARGS__) } while (0)
+#define _fmt_param(w, arg) \
+ LM_IF(LM_IS_TUPLE(arg))( \
+ _fmt_param_tuple LM_EAT() (w, LM_EXPAND arg) \
+ )( \
+ _fmt_param_nontuple(w, arg) \
+ );
+#define _fmt_param_tuple(w, fn, ...) fmt_print_##fn(w __VA_OPT__(,) __VA_ARGS__)
+#define _fmt_param_nontuple(w, val) _Generic((val), \
+ unsigned char : LM_CAT2_(_fmt_print_base10_u, __SCHAR_WIDTH__) , \
+ unsigned short : LM_CAT2_(_fmt_print_base10_u, __SHRT_WIDTH__) , \
+ unsigned int : LM_CAT2_(_fmt_print_base10_u, __INT_WIDTH__) , \
+ unsigned long : LM_CAT2_(_fmt_print_base10_u, __LONG_WIDTH__) , \
+ unsigned long long : LM_CAT2_(_fmt_print_base10_u, __LONG_LONG_WIDTH__) , \
+ signed char : LM_CAT2_(_fmt_print_base10_s, __SCHAR_WIDTH__) , \
+ signed short : LM_CAT2_(_fmt_print_base10_s, __SHRT_WIDTH__) , \
+ signed int : LM_CAT2_(_fmt_print_base10_s, __INT_WIDTH__) , \
+ signed long : LM_CAT2_(_fmt_print_base10_s, __LONG_WIDTH__) , \
+ signed long long : LM_CAT2_(_fmt_print_base10_s, __LONG_LONG_WIDTH__) , \
+ char * : fmt_print_str , \
+ const char * : fmt_print_str , \
+ bool : fmt_print_bool )(w, val)
+
+/** Same as fmt_print(), but usable from inside of fmt_print(). */
+#define fmt_print2(w, ...) do { LM_FOREACH_PARAM2_(_fmt_param2, (w), __VA_ARGS__) } while (0)
+#define _fmt_param2(...) _LM_DEFER2(_fmt_param_indirect)()(__VA_ARGS__)
+#define _fmt_param_indirect() _fmt_param
+
+/* print-to-memory ************************************************************/
+
+struct fmt_buf {
+ void *dat;
+ size_t len, cap;
+};
+LO_IMPLEMENTATION_H(fmt_dest, struct fmt_buf, fmt_buf);
+
+#define fmt_snprint(buf, n, ...) ({ \
+ struct fmt_buf _w = { .dat = buf, .cap = n }; \
+ lo_interface fmt_dest w = LO_BOX(fmt_dest, &_w); \
+ fmt_print(w, __VA_ARGS__); \
+ if (_w.len < _w.cap) \
+ ((char *)_w.dat)[_w.len] = '\0'; \
+ _w.len; \
+})
+
+#define fmt_asprint(...) ({ \
+ struct fmt_buf _w = {}; \
+ lo_interface fmt_dest w = LO_BOX(fmt_dest, &_w); \
+ fmt_print(w, __VA_ARGS__); \
+ while (_w.cap <= _w.len) { \
+ _w.cap = _w.len + 1; \
+ _w.len = 0; \
+ _w.dat = realloc(_w.dat, _w.cap); \
+ fmt_print(w, __VA_ARGS__); \
+ } \
+ ((char *)_w.dat)[_w.len] = '\0'; \
+ _w.dat; \
+})
+
+/* justify ********************************************************************/
+
+#define fmt_print_ljust(w, width, fillchar, ...) do { \
+ size_t beg = LO_CALL(w, tell); \
+ fmt_print2(w, __VA_ARGS__); \
+ while ((LO_CALL(w, tell) - beg) < width) \
+ fmt_print_byte(w, fillchar); \
+} while (0)
+
+#define fmt_print_rjust(w, width, fillchar, ...) do { \
+ struct fmt_buf _discard = {}; \
+ lo_interface fmt_dest discard = LO_BOX2(fmt_dest, &_discard); \
+ fmt_print2(discard, __VA_ARGS__); \
+ while (_discard.len++ < width) \
+ fmt_print_byte(w, fillchar); \
+ fmt_print2(w, __VA_ARGS__); \
+} while (0)
+
+void fmt_print_base16_u8_(lo_interface fmt_dest w, uint8_t x);
+void fmt_print_base16_u16_(lo_interface fmt_dest w, uint16_t x);
+void fmt_print_base16_u32_(lo_interface fmt_dest w, uint32_t x);
+void fmt_print_base16_u64_(lo_interface fmt_dest w, uint64_t x);
+void fmt_print_ptr(lo_interface fmt_dest w, void *ptr);
+
+#endif /* _LIBMISC_FMT_H_ */
diff --git a/libmisc/include/libmisc/hash.h b/libmisc/include/libmisc/hash.h
index 91e6b10..029bd3b 100644
--- a/libmisc/include/libmisc/hash.h
+++ b/libmisc/include/libmisc/hash.h
@@ -1,31 +1,21 @@
/* libmisc/hash.h - General-purpose hash utilities
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _LIBMISC_HASH_H_
#define _LIBMISC_HASH_H_
-#include <stdint.h> /* for uint{n}_t */
#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for uint{n}_t */
-/* djb2 hash */
-typedef uint32_t hash_t;
-static inline void hash_init(hash_t *hash) {
- *hash = 5381;
-}
-static inline void hash_write(hash_t *hash, void *dat, size_t len) {
- for (size_t i = 0; i < len; i++)
- *hash = (*hash * 33) + (hash_t)(((unsigned char *)dat)[i]);
-}
+/* base */
+typedef uint32_t hash_t; /* size subject to change */
+void hash_init(hash_t *hash);
+void hash_write(hash_t *hash, void *dat, size_t len);
/* utilities */
-static inline hash_t hash(void *dat, size_t len) {
- hash_t h;
- hash_init(&h);
- hash_write(&h, dat, len);
- return h;
-}
+hash_t hash(void *dat, size_t len);
#endif /* _LIBMISC_HASH_H_ */
diff --git a/libmisc/include/libmisc/linkedlist.h b/libmisc/include/libmisc/linkedlist.h
new file mode 100644
index 0000000..b6ff688
--- /dev/null
+++ b/libmisc/include/libmisc/linkedlist.h
@@ -0,0 +1,108 @@
+/* libmisc/linkedlist.h - Singly- and doubly- linked lists
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_LINKEDLIST_H_
+#define _LIBMISC_LINKEDLIST_H_
+
+/* low-level (intrusive) singly linked list ***********************************/
+
+struct _slist_node {
+ struct _slist_node *rear;
+};
+
+struct _slist_root {
+ struct _slist_node *front, *rear;
+};
+
+void _slist_push_to_rear(struct _slist_root *root, struct _slist_node *node);
+void _slist_pop_from_front(struct _slist_root *root);
+
+/* low-level (intrusive) doubly linked list ***********************************/
+
+struct _dlist_node {
+ struct _dlist_node *front, *rear;
+};
+
+struct _dlist_root {
+ struct _dlist_node *front, *rear;
+};
+
+void _dlist_push_to_rear(struct _dlist_root *root, struct _dlist_node *node);
+void _dlist_remove(struct _dlist_root *root, struct _dlist_node *node);
+void _dlist_pop_from_front(struct _dlist_root *root);
+
+/* singly linked list (non-intrusive) *****************************************/
+
+#define SLIST_DECLARE(NAME) \
+ struct NAME##_node; \
+ struct NAME { \
+ struct NAME##_node *front, *rear; \
+ struct NAME *_slist_root_typ[0]; \
+ }
+
+#define SLIST_DECLARE_NODE(NAME, VAL_T) \
+ struct NAME##_node { \
+ struct NAME##_node *rear; \
+ VAL_T val; \
+ }
+
+#define slist_push_to_rear(ROOT, NODE) { \
+ /* These temporary variables are to get the compiler to check \
+ * the types. */ \
+ typeof(*(ROOT)->_slist_root_typ[0]) *_rootp = ROOT; \
+ typeof(*_rootp->front) *_nodep = NODE; \
+ _slist_push_to_rear((struct _slist_root *)_rootp, \
+ (struct _slist_node *)_nodep); \
+} while (0)
+
+#define slist_pop_from_front(ROOT) { \
+ /* This temporary variables are to get the compiler to check \
+ * the type. */ \
+ typeof(*(ROOT)->_slist_root_typ[0]) *_rootp = ROOT; \
+ _slist_pop_from_front((struct _slist_root *)_rootp); \
+} while (0)
+
+/* doubly linked list (non-intrusive) *****************************************/
+
+#define DLIST_DECLARE(NAME) \
+ struct NAME##_node; \
+ struct NAME { \
+ struct NAME##_node *front, *rear; \
+ struct NAME *_dlist_root_typ[0]; \
+ }
+
+#define DLIST_DECLARE_NODE(NAME, VAL_T) \
+ struct NAME##_node { \
+ struct NAME##_node *front, *rear; \
+ VAL_T val; \
+ }
+
+#define dlist_push_to_rear(ROOT, NODE) { \
+ /* These temporary variables are to get the compiler to check \
+ * the types. */ \
+ typeof(*(ROOT)->_dlist_root_typ[0]) *_rootp = ROOT; \
+ typeof(*_rootp->front) *_nodep = NODE; \
+ _dlist_push_to_rear((struct _dlist_root *)_rootp, \
+ (struct _dlist_node *)_nodep); \
+} while (0)
+
+#define dlist_remove(ROOT, NODE) { \
+ /* These temporary variables are to get the compiler to check \
+ * the types. */ \
+ typeof(*(ROOT)->_dlist_root_typ[0]) *_rootp = ROOT; \
+ typeof(*_rootp->front) *_nodep = NODE; \
+ _dlist_remove((struct _dlist_root *)_rootp, \
+ (struct _dlist_node *)_nodep); \
+} while (0)
+
+#define dlist_pop_from_front(ROOT) { \
+ /* This temporary variables are to get the compiler to check \
+ * the type. */ \
+ typeof(*(ROOT)->_dlist_root_typ[0]) *_rootp = ROOT; \
+ _dlist_pop_from_front((struct _dlist_root *)_rootp); \
+} while (0)
+
+#endif /* _LIBMISC_LINKEDLIST_H_ */
diff --git a/libmisc/include/libmisc/log.h b/libmisc/include/libmisc/log.h
index 37da20b..c40b642 100644
--- a/libmisc/include/libmisc/log.h
+++ b/libmisc/include/libmisc/log.h
@@ -1,31 +1,37 @@
/* libmisc/log.h - stdio logging
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _LIBMISC_LOG_H_
#define _LIBMISC_LOG_H_
-#ifndef LOG_NAME
- #error "each compilation unit that includes <libmisc/log.h> must define LOG_NAME"
-#endif
+#include <stdint.h> /* for uint8_t */
+
+#include <libmisc/_intercept.h>
+#include <libmisc/fmt.h>
+#include <libmisc/macro.h>
#ifdef NDEBUG
#define _LOG_NDEBUG 1
#else
#define _LOG_NDEBUG 0
#endif
-#define __LOG_STR(x) #x
-#define _LOG_STR(x) __LOG_STR(x)
-#define __LOG_CAT3(a, b, c) a ## b ## c
-#define _LOG_CAT3(a, b, c) __LOG_CAT3(a, b, c)
-[[format(printf, 1, 2)]] int _log_printf(const char *format, ...);
+const char *const_byte_str(uint8_t b);
+
+extern lo_interface fmt_dest _log_dest;
-#define errorf(fmt, ...) do { _log_printf("error: " _LOG_STR(LOG_NAME) ": " fmt "\n" __VA_OPT__(,) __VA_ARGS__); } while (0)
-#define infof(fmt, ...) do { _log_printf("info : " _LOG_STR(LOG_NAME) ": " fmt "\n" __VA_OPT__(,) __VA_ARGS__); } while (0)
-#define debugf(fmt, ...) do { if (_LOG_CAT3(CONFIG_, LOG_NAME, _DEBUG) && !_LOG_NDEBUG) \
- _log_printf("debug: " _LOG_STR(LOG_NAME) ": " fmt "\n" __VA_OPT__(,) __VA_ARGS__); } while (0)
+#define log_n_errorln(nam, ...) fmt_print(_log_dest, "error: " LM_STR_(nam) ": ", __VA_ARGS__, "\n")
+#define log_n_infoln(nam, ...) fmt_print(_log_dest, "info : " LM_STR_(nam) ": ", __VA_ARGS__, "\n")
+#define log_n_debugln(nam, ...) do { if (LM_CAT3_(CONFIG_, nam, _DEBUG) && !_LOG_NDEBUG) \
+ fmt_print(_log_dest, "debug: " LM_STR_(nam) ": ", __VA_ARGS__, "\n"); } while (0)
#endif /* _LIBMISC_LOG_H_ */
+
+#if defined(LOG_NAME) && !defined(log_errorln)
+#define log_errorln(...) log_n_errorln(LOG_NAME, __VA_ARGS__)
+#define log_infoln(...) log_n_infoln(LOG_NAME, __VA_ARGS__)
+#define log_debugln(...) log_n_debugln(LOG_NAME, __VA_ARGS__)
+#endif
diff --git a/libmisc/include/libmisc/macro.h b/libmisc/include/libmisc/macro.h
new file mode 100644
index 0000000..48f52e5
--- /dev/null
+++ b/libmisc/include/libmisc/macro.h
@@ -0,0 +1,187 @@
+/* libmisc/macro.h - Useful C preprocessor macros
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_MACRO_H_
+#define _LIBMISC_MACRO_H_
+
+#include <libmisc/assert.h>
+
+/* C: syntax ******************************************************************/
+
+#define LM_FORCE_SEMICOLON static_assert(1, "force semicolon")
+
+#define LM_PARTIAL_SWITCH(VAL) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wswitch-enum\"") \
+ switch (VAL) \
+ _Pragma("GCC diagnostic pop") \
+
+/* C: function definitions ****************************************************/
+
+#define LM_UNUSED(argname)
+#define LM_ALWAYS_INLINE [[gnu::always_inline]] inline
+#define LM_NEVER_INLINE [[gnu::noinline]]
+#define LM_FLATTEN [[gnu::flatten]]
+
+/* C: types *******************************************************************/
+
+/* If it's a pointer instead of an array, then typeof(&ptr[0]) == typeof(ptr) */
+#define _LM_IS_ARRAY(ary) (!__builtin_types_compatible_p(typeof(&(ary)[0]), typeof(ary)))
+#define LM_ARRAY_LEN(ary) ( (sizeof(ary)/sizeof((ary)[0])) + static_assert_as_expr(_LM_IS_ARRAY(ary)) )
+
+#define LM_CAST_FIELD_TO_STRUCT(STRUCT_TYP, FIELD_NAME, PTR_TO_FIELD) ({ \
+ /* The _fptr assignment is to get the compiler to do type checking. */ \
+ typeof(((STRUCT_TYP *)NULL)->FIELD_NAME) *_fptr = (PTR_TO_FIELD); \
+ _fptr \
+ ? ((STRUCT_TYP*)( ((void*)_fptr) - offsetof(STRUCT_TYP, FIELD_NAME))) \
+ : NULL; \
+})
+
+/* C: numeric *****************************************************************/
+
+#define LM_CEILDIV(n, d) ( ((n)+(d)-1) / (d) ) /** Return ceil(n/d) */
+#define LM_ROUND_UP(n, d) ( LM_CEILDIV(n, d) * (d) ) /** Return `n` rounded up to the nearest multiple of `d` */
+#define LM_ROUND_DOWN(n, d) ( ((n)/(d)) * (d) ) /** Return `n` rounded down to the nearest multiple of `d` */
+#define LM_NEXT_POWER_OF_2(x) ( (x) ? 1ULL<<((sizeof(unsigned long long)*8)-__builtin_clzll(x)) : 1) /** Return the lowest power of 2 that is > x */
+#define LM_FLOORLOG2(x) ((sizeof(unsigned long long)*8)-__builtin_clzll(x)-1) /** Return floor(log_2(x) */
+
+/* CPP: strings ***************************************************************/
+
+#define LM_STR(x) #x
+#define LM_STR_(x) LM_STR(x)
+
+/* CPP: token pasting *********************************************************/
+
+#define LM_CAT2(a, b) a ## b
+#define LM_CAT3(a, b, c) a ## b ## c
+
+#define LM_CAT2_(a, b) LM_CAT2(a, b)
+#define LM_CAT3_(a, b, c) LM_CAT3(a, b, c)
+
+/* CPP: macro arguments *******************************************************/
+
+#define LM_FIRST(a, ...) a
+#define LM_SECOND(a, b, ...) b
+
+#define LM_FIRST_(...) LM_FIRST(__VA_ARGS__)
+#define LM_SECOND_(...) LM_SECOND(__VA_ARGS__)
+
+#define LM_EAT(...)
+#define LM_EXPAND(...) __VA_ARGS__
+
+/* CPP: conditionals **********************************************************/
+
+#define LM_T xxTxx
+#define LM_F xxFxx
+
+#define LM_SENTINEL() bogus, LM_T /* a magic sentinel value */
+#define LM_IS_SENTINEL(...) LM_SECOND(__VA_ARGS__, LM_F)
+
+#define LM_IF(cond) LM_CAT2(_LM_IF__, cond) /* LM_IF(cond)(then)(else) */
+#define _LM_IF__xxTxx(...) __VA_ARGS__ LM_EAT
+#define _LM_IF__xxFxx(...) LM_EXPAND
+
+/* CPP: tuples ****************************************************************/
+
+#define LM_IS_TUPLE(x) LM_IS_SENTINEL(_LM_IS_TUPLE x)
+#define _LM_IS_TUPLE(...) LM_SENTINEL()
+/* For LM_IS_EMPTY_TUPLE:
+ *
+ * Given
+ *
+ * #define HELPER(...) B, __VA_OPT__(C,) D
+ *
+ * then evaluating the sequence of tokens `HELPER x , A` will give us a
+ * new sequence of tokens according to the following table:
+ *
+ * not a tuple : HELPER x , A
+ * tuple, nonempty: B , C , D , A
+ * tuple, empty : B , D , A
+ *
+ * Looking at this table, it is clear that we must look at the 2nd
+ * resulting comma-separated-value (argument), and set A=false,
+ * C=false, D=true (and B doesn't matter).
+ */
+#define LM_IS_EMPTY_TUPLE(x) LM_SECOND_(_LM_IS_EMPTY_TUPLE x, LM_F)
+#define _LM_IS_EMPTY_TUPLE(...) bogus, __VA_OPT__(LM_F,) LM_T
+
+/* `tuples` is a sequence of `(tuple1)(tuple2)(tuple3)` */
+#define _LM_TUPLES_COMMA(...) (__VA_ARGS__),
+#define LM_TUPLES_IS_NONEMPTY(tuples) LM_IS_TUPLE(_LM_TUPLES_COMMA tuples)
+#define LM_TUPLES_HEAD(tuples) LM_EXPAND(LM_FIRST LM_EAT() (_LM_TUPLES_COMMA tuples))
+#define LM_TUPLES_TAIL(tuples) LM_EAT tuples
+
+/* CPP: iteration *************************************************************/
+
+/* The desire to support a high number of iterations is in competition
+ * with the desire for short compile times. 16 is the lowest
+ * power-of-2 for which the current code compiles. */
+#define _LM_EVAL _LM_EVAL__16
+#define _LM_EVAL__16(...) _LM_EVAL__8(_LM_EVAL__8(__VA_ARGS__))
+#define _LM_EVAL__8(...) _LM_EVAL__4(_LM_EVAL__4(__VA_ARGS__))
+#define _LM_EVAL__4(...) _LM_EVAL__2(_LM_EVAL__2(__VA_ARGS__))
+#define _LM_EVAL__2(...) _LM_EVAL__1(_LM_EVAL__1(__VA_ARGS__))
+#define _LM_EVAL__1(...) __VA_ARGS__
+
+#define _LM_DEFER2(macro) macro LM_EAT LM_EAT()()
+
+/**
+ * LM_FOREACH_PARAM(func, (fixedparams), params...) calls
+ * func(fixedparams..., param) for each param.
+ *
+ * BUG: LM_FOREACH_PARAM is limited to (16*2)-1=31 params.
+ */
+#define LM_FOREACH_PARAM(func, fixedparams, ...) _LM_EVAL(_LM_FOREACH_PARAM(func, fixedparams, __VA_ARGS__))
+#define _LM_FOREACH_PARAM(func, fixedparams, ...) _LM_FOREACH_PARAM_ITEM(func, fixedparams, __VA_ARGS__, ())
+#define _LM_FOREACH_PARAM_FIXEDPARAMS(fixedparams) _LM_FOREACH_PARAM_FIXEDPARAMS_inner fixedparams
+#define _LM_FOREACH_PARAM_FIXEDPARAMS_inner(...) __VA_ARGS__ __VA_OPT__(,)
+#define _LM_FOREACH_PARAM_ITEM(func, fixedparams, param, ...) \
+ LM_IF(LM_IS_EMPTY_TUPLE(param))()( \
+ _LM_DEFER2(func)(_LM_FOREACH_PARAM_FIXEDPARAMS(fixedparams) param) \
+ _LM_DEFER2(_LM_FOREACH_PARAM_ITEM_indirect)()(func, fixedparams, __VA_ARGS__) \
+ )
+#define _LM_FOREACH_PARAM_ITEM_indirect() _LM_FOREACH_PARAM_ITEM
+
+/** The same as LM_FOREACH_PARAM(), but callable from inside of LM_FOREACH_PARAM(). */
+#define LM_FOREACH_PARAM2(...) _LM_DEFER2(_LM_FOREACH_PARAM_ITEM_indirect)()(__VA_ARGS__, ())
+
+/** The same as above, but evaluates the arguments first. */
+#define LM_FOREACH_PARAM_(...) LM_FOREACH_PARAM(__VA_ARGS__)
+#define LM_FOREACH_PARAM2_(...) LM_FOREACH_PARAM2(__VA_ARGS__)
+
+/**
+ * LM_FOREACH_TUPLE( (tup1) (tup2) (tup3), func, args...) calls
+ * func(args..., tup...) for each tuple.
+ *
+ * BUG: LM_FOREACH_TUPLE is limited to (16*2)-1=31 tuples.
+ */
+#define LM_FOREACH_TUPLE(tuples, func, ...) \
+ _LM_EVAL(_LM_FOREACH_TUPLE(tuples, func, __VA_ARGS__))
+#define _LM_FOREACH_TUPLE(tuples, func, ...) \
+ LM_IF(LM_TUPLES_IS_NONEMPTY(tuples))( \
+ _LM_DEFER2(func)(__VA_ARGS__ __VA_OPT__(,) LM_EXPAND LM_TUPLES_HEAD(tuples)) \
+ _LM_DEFER2(_LM_FOREACH_TUPLE_indirect)()(LM_TUPLES_TAIL(tuples), func, __VA_ARGS__) \
+ )()
+#define _LM_FOREACH_TUPLE_indirect() _LM_FOREACH_TUPLE
+
+/** The same as LM_FOREACH_TUPLE(), but callable from inside of LM_FOREACH_TUPLE(). */
+#define LM_FOREACH_TUPLE2(...) _LM_DEFER2(_LM_FOREACH_TUPLE_indirect)()(__VA_ARGS__)
+
+/* CPP: wrap-cc extensions ****************************************************/
+
+#ifdef __LIBMISC_ENHANCED_CPP__
+/**
+ * `LM_DEFAPPEND(macro, val)` is like `#define macro val`, but can (1)
+ * be used from inside of a macro, and (2) appends to a value if it is
+ * already defined with LM_DEFAPPEND. There are lots of edge-cases,
+ * don't get cute.
+ */
+#define LM_DEFAPPEND(macro, ...) __xx__LM_DEFAPPEND__xx__(#macro, #__VA_ARGS__) LM_FORCE_SEMICOLON
+#define LM_DEFAPPEND_(macro, ...) _LM_DEFAPPEND_(#macro, __VA_ARGS__)
+#define _LM_DEFAPPEND_(macrostr, ...) __xx__LM_DEFAPPEND__xx__(macrostr, #__VA_ARGS__) LM_FORCE_SEMICOLON
+#endif
+
+#endif /* _LIBMISC_MACRO_H_ */
diff --git a/libmisc/include/libmisc/map.h b/libmisc/include/libmisc/map.h
new file mode 100644
index 0000000..113bc0e
--- /dev/null
+++ b/libmisc/include/libmisc/map.h
@@ -0,0 +1,145 @@
+/* libmisc/map.h - A map/dict data structure
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_MAP_H_
+#define _LIBMISC_MAP_H_
+
+#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for uint8_t */
+
+#include <libmisc/linkedlist.h>
+
+/* Type ***********************************************************************/
+
+DLIST_DECLARE(_map_kv_list);
+
+struct _map {
+ size_t len;
+ size_t nbuckets;
+ struct _map_kv_list *buckets;
+
+ unsigned int iterating;
+
+ size_t sizeof_kv;
+ size_t offsetof_k, sizeof_k;
+ size_t offsetof_v, sizeof_v;
+};
+
+/**
+ * MAP_DECLARE(MAPNAME, KEY_T, VAL_T) declares `struct MAPNAME`.
+ */
+#define MAP_DECLARE(MAPNAME, KEY_T, VAL_T) \
+ struct _##MAPNAME##_kv { \
+ uint8_t flags; \
+ KEY_T key; \
+ VAL_T val; \
+ }; \
+ DLIST_DECLARE_NODE(_##MAPNAME##_kv_list, struct _##MAPNAME##_kv); \
+ struct MAPNAME { \
+ struct _map core; \
+ struct _##MAPNAME##_kv_list_node kv_typ[0]; \
+ }
+
+#define _map_init(M) do { \
+ if (!(M)->core.sizeof_kv) { \
+ (M)->core.sizeof_kv = sizeof((M)->kv_typ[0]); \
+ (M)->core.sizeof_k = sizeof((M)->kv_typ[0].val.key); \
+ (M)->core.sizeof_v = sizeof((M)->kv_typ[0].val.val); \
+ (M)->core.offsetof_k = offsetof(typeof((M)->kv_typ[0]), val.key); \
+ (M)->core.offsetof_v = offsetof(typeof((M)->kv_typ[0]), val.val); \
+ } \
+} while (0)
+
+/* Methods ********************************************************************/
+
+/**
+ * map_len(map) returns the number of entries currently in `map`.
+ */
+#define map_len(M) ((M)->core.len)
+
+/**
+ * map_load(map, key) returns a pointer to the value in `map`
+ * associated with `key`, or else NULL.
+ */
+#define map_load(M, K) ({ \
+ _map_init(M); \
+ typeof((M)->kv_typ[0].val.key) _k = K; \
+ (typeof((M)->kv_typ[0].val.val)*)_map_load(&(M)->core, &_k); \
+})
+void *_map_load(struct _map *m, void *kp);
+
+/**
+ * map_del(map, key) ensures that `key` is not present in `map`.
+ * Returns whether `key` was in `map` before the call.
+ */
+#define map_del(M, K) ({ \
+ _map_init(M); \
+ typeof((M)->kv_typ[0].val.key) _k = K; \
+ _map_del(&(M)->core, &_k); \
+})
+bool _map_del(struct _map *m, void *kp);
+
+/**
+ * map_store(map, key, val) sets a value in the map. Returns a
+ * pointer to the map's copy of `val`.
+ */
+#define map_store(M, K, ...) ({ \
+ _map_init(M); \
+ typeof((M)->kv_typ[0].val.key) _k = K; \
+ typeof((M)->kv_typ[0].val.val) _v = __VA_ARGS__; \
+ (typeof((M)->kv_typ[0].val.val)*)_map_store(&(M)->core, &_k, &_v); \
+})
+void *_map_store(struct _map *m, void *kp, void *vp);
+
+/**
+ * map_free(map) frees the memory associated with the map.
+ */
+#define map_free(M) _map_free(&(M)->core);
+void _map_free(struct _map *m);
+
+/* Iteration ******************************************************************/
+
+struct _map_iter {
+ struct _map *m;
+ void *keyp;
+ void **valpp;
+
+ size_t i;
+ struct _map_kv_list_node *kv;
+};
+
+/**
+ * MAP_FOREACH iterates over a map:
+ *
+ * MAP_FOREACH(MAP_EACH(MAP, key, valp)) {
+ * ...
+ * }
+ *
+ * It is safe to mutate the map with map_store() and, map_del() while
+ * iterating, but entries added by map_store() may or may not be
+ * visited by the iteration.
+ */
+#define MAP_FOREACH(M, KNAME, VNAME) _MAP_FOREACH(__COUNTER__, M, KNAME, VNAME)
+#define _MAP_FOREACH(CNT, M, KNAME, VNAME) \
+ for (bool _once_##CNT = true; _once_##CNT;) \
+ for (typeof((M)->kv_typ[0].val.key) KNAME; _once_##CNT;) \
+ for (typeof((M)->kv_typ[0].val.val) *VNAME; _once_##CNT;) \
+ for ( \
+ struct _map_iter _iter_##CNT = ({ \
+ _map_init(M); \
+ _map_iter_before(&(M)->core, &KNAME, (void**)&VNAME); \
+ }); \
+ _once_##CNT; \
+ ({ \
+ _once_##CNT = false; \
+ _map_iter_after(&_iter_##CNT); \
+ })) \
+ while (_map_iter_next(&_iter_##CNT))
+struct _map_iter _map_iter_before(struct _map *m, void *keyp, void **valpp);
+bool _map_iter_next(struct _map_iter *iter);
+void _map_iter_after(struct _map_iter *iter);
+
+#endif /* _LIBMISC_MAP_H_ */
diff --git a/libmisc/include/libmisc/obj.h b/libmisc/include/libmisc/obj.h
new file mode 100644
index 0000000..6afa391
--- /dev/null
+++ b/libmisc/include/libmisc/obj.h
@@ -0,0 +1,164 @@
+/* libmisc/obj.h - A simple Go-ish object system
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_OBJ_H_
+#define _LIBMISC_OBJ_H_
+
+#include <libmisc/macro.h>
+
+/**
+ * Use `lo_interface` similarly to how you would use
+ * `struct`/`enum`/`union` when writing the type of an interface
+ * value.
+ */
+#define lo_interface struct
+
+/**
+ * Use `LO_INTERFACE` in a .h file to define an interface.
+ *
+ * First define a macro named `{iface_name}_LO_IFACE` consisting of a
+ * series of calls to LO_NEST and/or LO_FUNC, then call
+ * `LO_INTERFACE({iface_name})`:
+ *
+ * #define myiface_LO_IFACE \
+ * LO_NEST(wrapped_iface_name) \
+ * LO_FUNC(ret_type, func_name, args...)
+ * LO_INTERFACE(myiface)
+ *
+ * Use `lo_interface {iface_name}` as the type of this interface; it
+ * should not be a pointer type.
+ */
+#define LO_NEST(_ARG_child_iface_name) \
+ (lo_nest, _ARG_child_iface_name)
+#define LO_FUNC(_ARG_ret_type, _ARG_func_name, ...) \
+ (lo_func, _ARG_ret_type, _ARG_func_name __VA_OPT__(,) __VA_ARGS__)
+#define LO_INTERFACE(_ARG_iface_name) \
+ struct _lo_##_ARG_iface_name##_vtable { \
+ LM_FOREACH_TUPLE(_ARG_iface_name##_LO_IFACE, \
+ _LO_IFACE_VTABLE) \
+ }; \
+ struct _ARG_iface_name { \
+ void *self; \
+ const struct _lo_##_ARG_iface_name##_vtable *vtable; \
+ }; \
+ LM_FOREACH_TUPLE(_ARG_iface_name##_LO_IFACE, \
+ _LO_IFACE_PROTO, _ARG_iface_name) \
+ LM_FORCE_SEMICOLON
+
+#define _LO_IFACE_VTABLE(_tuple_typ, ...) _LO_IFACE_VTABLE_##_tuple_typ(__VA_ARGS__)
+#define _LO_IFACE_VTABLE_lo_nest(_ARG_child_iface_name) \
+ const struct _lo_##_ARG_child_iface_name##_vtable *_lo_##_ARG_child_iface_name##_vtable; \
+ LM_FOREACH_TUPLE2(_ARG_child_iface_name##_LO_IFACE, _LO_IFACE_VTABLE2)
+#define _LO_IFACE_VTABLE_lo_func(_ARG_ret_type, _ARG_func_name, ...) \
+ _ARG_ret_type (*_ARG_func_name)(void * __VA_OPT__(,) __VA_ARGS__);
+
+#define _LO_IFACE_VTABLE_indirect() _LO_IFACE_VTABLE
+#define _LO_IFACE_VTABLE2(...) _LM_DEFER2(_LO_IFACE_VTABLE_indirect)()(__VA_ARGS__)
+
+#define _LO_IFACE_PROTO(_ARG_iface_name, _tuple_typ, ...) _LO_IFACE_PROTO_##_tuple_typ(_ARG_iface_name, __VA_ARGS__)
+#define _LO_IFACE_PROTO_lo_nest(_ARG_iface_name, _ARG_child_iface_name) \
+ LM_DEFAPPEND(_LO_REGISTRY_##_ARG_child_iface_name, \
+ (lo_interface _ARG_iface_name, _LO_BOX_##_ARG_iface_name##_as_##_ARG_child_iface_name)); \
+ LM_DEFAPPEND(_LO_BOX_##_ARG_iface_name##_as_##_ARG_child_iface_name(obj), \
+ { .self = obj.self, .vtable = obj.vtable->_lo_##_ARG_child_iface_name##_vtable });
+#define _LO_IFACE_PROTO_lo_func(_ARG_iface_name, _ARG_ret_type, _ARG_func_name, ...) \
+ /* empty */
+
+/**
+ * `LO_BOX(iface_name, obj)` boxes `obj` as a `lo_interface
+ * iface_name`. `obj` must be one of:
+ *
+ * - A pointer to a value that implements `lo_interface iface_name`
+ * - An already-boxed instance of `lo_interface iface_name`
+ * - An already-boxed instance of another interface that
+ * `iface_name` inherits from.
+ */
+#define LO_BOX(_ARG_iface_name, obj) _Generic((obj), \
+ lo_interface _ARG_iface_name: obj \
+ LM_FOREACH_TUPLE(_LO_REGISTRY_##_ARG_iface_name, \
+ _LO_BOX, _ARG_iface_name, obj))
+#define LO_BOX2(_ARG_iface_name, obj) _Generic((obj), \
+ lo_interface _ARG_iface_name: obj \
+ LM_FOREACH_TUPLE2(_LO_REGISTRY_##_ARG_iface_name, \
+ _LO_BOX, _ARG_iface_name, obj))
+#define _LO_BOX(_ARG_iface_name, obj, typ, boxfn) \
+ , typ: (lo_interface _ARG_iface_name)boxfn(obj)
+
+/**
+ * `LO_NULL(iface_name)` is the null/nil/zero value for `lo_interface {iface_name}`.
+ */
+#define LO_NULL(_ARG_iface_name) ((lo_interface _ARG_iface_name){})
+
+/**
+ * `LO_IS_NULL(iface_val)` returns whether `iface_val` is LO_NULL.
+ */
+#define LO_IS_NULL(_ARG_iface_val) ((_ARG_iface_val).vtable == NULL)
+
+/**
+ * `LO_IFACE_EQ(a, b)` returns whether the interface values `a` and
+ * `b` are the same object.
+ */
+#define LO_EQ(_ARG_iface_val_a, _ARG_iface_val_b) \
+ ((_ARG_iface_val_a).self == (_ARG_iface_val_b).self)
+
+/**
+ * Use LO_CALL(obj, method_name, args...) to call a method on an `lo_interface`.
+ */
+#define LO_CALL(_ARG_obj, _ARG_meth, ...) \
+ (_ARG_obj).vtable->_ARG_meth((_ARG_obj).self __VA_OPT__(,) __VA_ARGS__)
+
+/**
+ * Use `LO_IMPLEMENTATION_H(iface_name, impl_type, impl_name)` in a .h
+ * file to declare that `{impl_type}` implements the `{iface_name}`
+ * interface with functions named `{impl_name}_{method_name}`.
+ *
+ * You must also call the LO_IMPLEMENTATION_C in a single .c file.
+ */
+#define LO_IMPLEMENTATION_H(_ARG_iface_name, _ARG_impl_type, _ARG_impl_name) \
+ /* Vtable. */ \
+ extern const struct _lo_##_ARG_iface_name##_vtable \
+ _lo_##_ARG_impl_name##_##_ARG_iface_name##_vtable; \
+ /* Boxing. */ \
+ LM_DEFAPPEND(_LO_REGISTRY_##_ARG_iface_name, \
+ (_ARG_impl_type *, _LO_BOX_##_ARG_impl_name##_as_##_ARG_iface_name)); \
+ LM_DEFAPPEND(_LO_BOX_##_ARG_impl_name##_as_##_ARG_iface_name(obj), \
+ { .self = obj, .vtable = &_lo_##_ARG_impl_name##_##_ARG_iface_name##_vtable }); \
+ LM_FORCE_SEMICOLON
+
+/**
+ * Use `LO_IMPLEMENTATION_C(iface_name, impl_type, impl_name[, static])` in a .c
+ * file to declare that `{impl_type}` implements the `{iface_name}` interface
+ * with functions named `{impl_name}_{method_name}`.
+ *
+ * You must also call the LO_IMPLEMENTATION_H in the corresponding .h file.
+ *
+ * If `iface_name` contains a nested interface, then the
+ * implementation of the nested interfaces must be declared with
+ * `LO_IMPLEMENTATION_C` first.
+ */
+#define LO_IMPLEMENTATION_C(_ARG_iface_name, _ARG_impl_type, _ARG_impl_name, ...) \
+ /* Method prototypes. */ \
+ LM_FOREACH_TUPLE(_ARG_iface_name##_LO_IFACE, \
+ _LO_IMPL_PROTO, _ARG_impl_type, _ARG_impl_name, __VA_ARGS__) \
+ /* Vtable. */ \
+ const struct _lo_##_ARG_iface_name##_vtable \
+ _lo_##_ARG_impl_name##_##_ARG_iface_name##_vtable = { \
+ LM_FOREACH_TUPLE(_ARG_iface_name##_LO_IFACE, \
+ _LO_IMPL_VTABLE, _ARG_impl_name) \
+ }
+
+#define _LO_IMPL_PROTO( _ARG_impl_type, _ARG_impl_name, _ARG_quals, _tuple_typ, ...) _LO_IMPL_PROTO_##_tuple_typ(_ARG_impl_type, _ARG_impl_name, _ARG_quals, __VA_ARGS__)
+#define _LO_IMPL_PROTO_lo_nest(_ARG_impl_type, _ARG_impl_name, _ARG_quals, _ARG_child_iface_name) /* empty */
+#define _LO_IMPL_PROTO_lo_func(_ARG_impl_type, _ARG_impl_name, _ARG_quals, _ARG_ret_type, _ARG_func_name, ...) _ARG_quals _ARG_ret_type _ARG_impl_name##_##_ARG_func_name(_ARG_impl_type * __VA_OPT__(,) __VA_ARGS__);
+
+#define _LO_IMPL_VTABLE(_ARG_impl_name, _tuple_typ, ...) _LO_IMPL_VTABLE_##_tuple_typ(_ARG_impl_name, __VA_ARGS__)
+#define _LO_IMPL_VTABLE_lo_nest(_ARG_impl_name, _ARG_child_iface_name) ._lo_##_ARG_child_iface_name##_vtable = &_lo_##_ARG_impl_name##_##_ARG_child_iface_name##_vtable, LM_FOREACH_TUPLE2(_ARG_child_iface_name##_LO_IFACE, _LO_IMPL_VTABLE2, _ARG_impl_name)
+#define _LO_IMPL_VTABLE_lo_func(_ARG_impl_name, _ARG_ret_type, _ARG_func_name, ...) ._ARG_func_name = (void*)_ARG_impl_name##_##_ARG_func_name,
+
+#define _LO_IMPL_VTABLE_indirect() _LO_IMPL_VTABLE
+#define _LO_IMPL_VTABLE2(...) _LM_DEFER2(_LO_IMPL_VTABLE_indirect)()(__VA_ARGS__)
+
+#endif /* _LIBMISC_OBJ_H_ */
diff --git a/libmisc/include/libmisc/private.h b/libmisc/include/libmisc/private.h
index bc5e7ad..5a8777c 100644
--- a/libmisc/include/libmisc/private.h
+++ b/libmisc/include/libmisc/private.h
@@ -1,37 +1,17 @@
/* libmisc/private.h - Utilities to hide struct fields
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _LIBMISC_PRIVATE_H_
#define _LIBMISC_PRIVATE_H_
-/* primitive utilities */
+#include <libmisc/macro.h>
-#define _SECOND(a, b, ...) b
-
-#define _CAT(a, b) a ## b
-#define _CAT2(a, b) _CAT(a, b)
-#define _EAT(...)
-#define _EXPAND(...) __VA_ARGS__
-
-/* conditionals */
-
-#define _T xxTxx
-#define _F xxFxx
-
-#define _SENTINEL() bogus, _T /* a magic sentinel value */
-#define _IS_SENTINEL(...) _SECOND(__VA_ARGS__, _F)
-
-#define _IF(cond) _CAT(_IF__, cond) /* _IF(cond)(then)(else) */
-#define _IF__xxTxx(...) __VA_ARGS__ _EAT
-#define _IF__xxFxx(...) _EXPAND
-
-/* high-level functionality */
-
-#define YES _SENTINEL()
-#define BEGIN_PRIVATE(name) _IF(_IS_SENTINEL(IMPLEMENTATION_FOR_##name))()(struct {)
-#define END_PRIVATE(name) _IF(_IS_SENTINEL(IMPLEMENTATION_FOR_##name))()(} _CAT2(_HIDDEN_, __COUNTER__);)
+#define YES LM_SENTINEL()
+#define IS_IMPLEMENTATION_FOR(name) LM_IS_SENTINEL(IMPLEMENTATION_FOR_##name)
+#define BEGIN_PRIVATE(name) LM_IF(IS_IMPLEMENTATION_FOR(name))()(struct {) LM_FORCE_SEMICOLON
+#define END_PRIVATE(name) LM_IF(IS_IMPLEMENTATION_FOR(name))(LM_FORCE_SEMICOLON)(} LM_CAT2_(_PRIVATE_, __COUNTER__))
#endif /* _LIBMISC_PRIVATE_H_ */
diff --git a/libmisc/include/libmisc/rand.h b/libmisc/include/libmisc/rand.h
index 8072841..ca16f42 100644
--- a/libmisc/include/libmisc/rand.h
+++ b/libmisc/include/libmisc/rand.h
@@ -1,46 +1,18 @@
/* libmisc/rand.h - Non-crytpographic random-number utilities
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#ifndef _LIBMISC_RAND_H_
#define _LIBMISC_RAND_H_
-#include <stdint.h> /* for uint{n}_t, UINT{n}_C() */
-#include <stdlib.h> /* for random() */
-
-#include <libmisc/assert.h>
+#include <stdint.h> /* for uint{n}_t */
/**
* Return a psuedo-random number in the half-open interval [0,cnt).
* `cnt` must not be greater than 1<<63.
*/
-static inline uint64_t rand_uint63n(uint64_t cnt) {
- assert(cnt != 0 && ((cnt-1) & 0x8000000000000000) == 0);
- if (cnt <= UINT64_C(1)<<31) {
- uint32_t fair_cnt = ((UINT32_C(1)<<31) / cnt) * cnt;
- uint32_t rnd;
- do {
- rnd = random();
- } while (rnd >= fair_cnt);
- return rnd % cnt;
- } else if (cnt <= UINT64_C(1)<<62) {
- uint64_t fair_cnt = ((UINT64_C(1)<<62) / cnt) * cnt;
- uint64_t rnd;
- do {
- rnd = (random() << 31) | random();
- } while (rnd >= fair_cnt);
- return rnd % cnt;
- } else if (cnt <= UINT64_C(1)<<63) {
- uint64_t fair_cnt = ((UINT64_C(1)<<63) / cnt) * cnt;
- uint64_t rnd;
- do {
- rnd = (random() << 62) | (random() << 31) | random();
- } while (rnd >= fair_cnt);
- return rnd % cnt;
- }
- assert_notreached("cnt is out of bounds");
-}
+uint64_t rand_uint63n(uint64_t cnt);
#endif /* _LIBMISC_RAND_H_ */
diff --git a/libmisc/include/libmisc/utf8.h b/libmisc/include/libmisc/utf8.h
new file mode 100644
index 0000000..54fcc92
--- /dev/null
+++ b/libmisc/include/libmisc/utf8.h
@@ -0,0 +1,25 @@
+/* libmisc/utf8.h - UTF-8 routines
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBMISC_UTF8_H_
+#define _LIBMISC_UTF8_H_
+
+#include <stddef.h> /* for size_t */
+#include <stdint.h> /* for uint{n}_t */
+
+/**
+ * Decode the codepoint starting at `str` and consuming at most `len`
+ * bytes. Invalid UTF-8 is indicated with chlen=0. For valid UTF-8,
+ * chlen is always in the range [1, 4].
+ */
+void utf8_decode_codepoint(const uint8_t *str, size_t len, uint32_t *ret_ch, uint8_t *ret_chlen);
+
+bool _utf8_is_valid(const uint8_t *str, size_t len, bool forbid_nul);
+
+#define utf8_is_valid(str, len) _utf8_is_valid(str, len, false)
+#define utf8_is_valid_without_nul(str, len) _utf8_is_valid(str, len, true)
+
+#endif /* _LIBMISC_UTF8_H_ */
diff --git a/libmisc/include/libmisc/vcall.h b/libmisc/include/libmisc/vcall.h
deleted file mode 100644
index 9b54c06..0000000
--- a/libmisc/include/libmisc/vcall.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/* libmisc/vcall.h - A simple Go-ish object system built on GCC -fplan9-extensions
- *
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-#ifndef _LIBMISC_VCALL_H_
-#define _LIBMISC_VCALL_H_
-
-#include <stddef.h> /* for offsetof() */
-
-#include <libmisc/assert.h>
-
-#define VCALL(o, m, ...) \
- ({ \
- assert(o); \
- (o)->vtable->m(o __VA_OPT__(,) __VA_ARGS__); \
- })
-
-#define VCALL_SELF(obj_typ, iface_typ, iface_ptr) \
- ({ \
- static_assert(_Generic(iface_ptr, iface_typ *: 1, default: 0), \
- "typeof("#iface_ptr") != "#iface_typ); \
- assert(iface_ptr); \
- ((obj_typ*)(((void*)iface_ptr)-offsetof(obj_typ,iface_typ))); \
- })
-
-#endif /* _LIBMISC_VCALL_H_ */
diff --git a/libmisc/intercept.c b/libmisc/intercept.c
new file mode 100644
index 0000000..d0e3602
--- /dev/null
+++ b/libmisc/intercept.c
@@ -0,0 +1,20 @@
+/* libmisc/intercept.c - Interceptable ("weak") functions
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdio.h> /* for putchar() */
+#include <stdlib.h> /* for abort() */
+
+#include <libmisc/_intercept.h>
+
+[[gnu::weak]]
+void __lm_putchar(unsigned char c) {
+ (void)putchar(c);
+}
+
+[[gnu::weak]]
+void __lm_abort(void) {
+ abort();
+}
diff --git a/libmisc/linkedlist.c b/libmisc/linkedlist.c
new file mode 100644
index 0000000..71a0aa9
--- /dev/null
+++ b/libmisc/linkedlist.c
@@ -0,0 +1,64 @@
+/* libmisc/linkedlist.c - Singly- and doubly- linked lists
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stddef.h> /* for NULL */
+
+#include <libmisc/assert.h>
+
+#include <libmisc/linkedlist.h>
+
+/* singly linked list *********************************************************/
+
+void _slist_push_to_rear(struct _slist_root *root, struct _slist_node *node) {
+ assert(root);
+ node->rear = NULL;
+ if (root->rear)
+ root->rear->rear = node;
+ else
+ root->front = node;
+ root->rear = node;
+}
+
+void _slist_pop_from_front(struct _slist_root *root) {
+ assert(root);
+ assert(root->front);
+ root->front = root->front->rear;
+ if (!root->front)
+ root->rear = NULL;
+}
+
+/* doubly linked list *********************************************************/
+
+void _dlist_push_to_rear(struct _dlist_root *root, struct _dlist_node *node) {
+ assert(root);
+ assert(node);
+ node->front = root->rear;
+ node->rear = NULL;
+ if (root->rear)
+ root->rear->rear = node;
+ else
+ root->front = node;
+ root->rear = node;
+}
+
+void _dlist_remove(struct _dlist_root *root, struct _dlist_node *node) {
+ assert(root);
+ assert(node);
+ if (node->front)
+ node->front->rear = node->rear;
+ else
+ root->front = node->rear;
+ if (node->rear)
+ node->rear->front = node->front;
+ else
+ root->rear = node->front;
+}
+
+void _dlist_pop_from_front(struct _dlist_root *root) {
+ assert(root);
+ assert(root->front);
+ _dlist_remove(root, root->front);
+}
diff --git a/libmisc/log.c b/libmisc/log.c
index a1ec10f..7e917c6 100644
--- a/libmisc/log.c
+++ b/libmisc/log.c
@@ -1,19 +1,293 @@
/* libmisc/log.c - stdio logging
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-#include <stdio.h> /* for vprintf() */
-#include <stdarg.h> /* for va_list, va_start(), va_end() */
+#include <stdint.h> /* for uint{n}_t */
-#define LOG_NAME
+#include <libmisc/assert.h> /* for static_assert() */
+
+#include <libmisc/_intercept.h>
#include <libmisc/log.h>
-int _log_printf(const char *format, ...) {
- va_list va;
- va_start(va, format);
- int ret = vprintf(format, va);
- va_end(va);
- return ret;
+struct log_stdout {};
+LO_IMPLEMENTATION_H(fmt_dest, struct log_stdout, log_stdout);
+LO_IMPLEMENTATION_C(fmt_dest, struct log_stdout, log_stdout, static);
+
+static size_t log_bytes = 0;
+
+static void log_stdout_putb(struct log_stdout *, uint8_t b) {
+ __lm_putchar(b);
+ log_bytes++;
+}
+
+static size_t log_stdout_tell(struct log_stdout *) {
+ return log_bytes;
+}
+
+lo_interface fmt_dest _log_dest = { .vtable = &_lo_log_stdout_fmt_dest_vtable };
+
+static const char *byte_strs[] = {
+ "0x00",
+ "0x01",
+ "0x02",
+ "0x03",
+ "0x04",
+ "0x05",
+ "0x06",
+ "0x07",
+ "0x08",
+ "0x09",
+ "0x0A",
+ "0x0B",
+ "0x0C",
+ "0x0D",
+ "0x0E",
+ "0x0F",
+ "0x10",
+ "0x11",
+ "0x12",
+ "0x13",
+ "0x14",
+ "0x15",
+ "0x16",
+ "0x17",
+ "0x18",
+ "0x19",
+ "0x1A",
+ "0x1B",
+ "0x1C",
+ "0x1D",
+ "0x1E",
+ "0x1F",
+ "0x20",
+ "0x21",
+ "0x22",
+ "0x23",
+ "0x24",
+ "0x25",
+ "0x26",
+ "0x27",
+ "0x28",
+ "0x29",
+ "0x2A",
+ "0x2B",
+ "0x2C",
+ "0x2D",
+ "0x2E",
+ "0x2F",
+ "0x30",
+ "0x31",
+ "0x32",
+ "0x33",
+ "0x34",
+ "0x35",
+ "0x36",
+ "0x37",
+ "0x38",
+ "0x39",
+ "0x3A",
+ "0x3B",
+ "0x3C",
+ "0x3D",
+ "0x3E",
+ "0x3F",
+ "0x40",
+ "0x41",
+ "0x42",
+ "0x43",
+ "0x44",
+ "0x45",
+ "0x46",
+ "0x47",
+ "0x48",
+ "0x49",
+ "0x4A",
+ "0x4B",
+ "0x4C",
+ "0x4D",
+ "0x4E",
+ "0x4F",
+ "0x50",
+ "0x51",
+ "0x52",
+ "0x53",
+ "0x54",
+ "0x55",
+ "0x56",
+ "0x57",
+ "0x58",
+ "0x59",
+ "0x5A",
+ "0x5B",
+ "0x5C",
+ "0x5D",
+ "0x5E",
+ "0x5F",
+ "0x60",
+ "0x61",
+ "0x62",
+ "0x63",
+ "0x64",
+ "0x65",
+ "0x66",
+ "0x67",
+ "0x68",
+ "0x69",
+ "0x6A",
+ "0x6B",
+ "0x6C",
+ "0x6D",
+ "0x6E",
+ "0x6F",
+ "0x70",
+ "0x71",
+ "0x72",
+ "0x73",
+ "0x74",
+ "0x75",
+ "0x76",
+ "0x77",
+ "0x78",
+ "0x79",
+ "0x7A",
+ "0x7B",
+ "0x7C",
+ "0x7D",
+ "0x7E",
+ "0x7F",
+ "0x80",
+ "0x81",
+ "0x82",
+ "0x83",
+ "0x84",
+ "0x85",
+ "0x86",
+ "0x87",
+ "0x88",
+ "0x89",
+ "0x8A",
+ "0x8B",
+ "0x8C",
+ "0x8D",
+ "0x8E",
+ "0x8F",
+ "0x90",
+ "0x91",
+ "0x92",
+ "0x93",
+ "0x94",
+ "0x95",
+ "0x96",
+ "0x97",
+ "0x98",
+ "0x99",
+ "0x9A",
+ "0x9B",
+ "0x9C",
+ "0x9D",
+ "0x9E",
+ "0x9F",
+ "0xA0",
+ "0xA1",
+ "0xA2",
+ "0xA3",
+ "0xA4",
+ "0xA5",
+ "0xA6",
+ "0xA7",
+ "0xA8",
+ "0xA9",
+ "0xAA",
+ "0xAB",
+ "0xAC",
+ "0xAD",
+ "0xAE",
+ "0xAF",
+ "0xB0",
+ "0xB1",
+ "0xB2",
+ "0xB3",
+ "0xB4",
+ "0xB5",
+ "0xB6",
+ "0xB7",
+ "0xB8",
+ "0xB9",
+ "0xBA",
+ "0xBB",
+ "0xBC",
+ "0xBD",
+ "0xBE",
+ "0xBF",
+ "0xC0",
+ "0xC1",
+ "0xC2",
+ "0xC3",
+ "0xC4",
+ "0xC5",
+ "0xC6",
+ "0xC7",
+ "0xC8",
+ "0xC9",
+ "0xCA",
+ "0xCB",
+ "0xCC",
+ "0xCD",
+ "0xCE",
+ "0xCF",
+ "0xD0",
+ "0xD1",
+ "0xD2",
+ "0xD3",
+ "0xD4",
+ "0xD5",
+ "0xD6",
+ "0xD7",
+ "0xD8",
+ "0xD9",
+ "0xDA",
+ "0xDB",
+ "0xDC",
+ "0xDD",
+ "0xDE",
+ "0xDF",
+ "0xE0",
+ "0xE1",
+ "0xE2",
+ "0xE3",
+ "0xE4",
+ "0xE5",
+ "0xE6",
+ "0xE7",
+ "0xE8",
+ "0xE9",
+ "0xEA",
+ "0xEB",
+ "0xEC",
+ "0xED",
+ "0xEE",
+ "0xEF",
+ "0xF0",
+ "0xF1",
+ "0xF2",
+ "0xF3",
+ "0xF4",
+ "0xF5",
+ "0xF6",
+ "0xF7",
+ "0xF8",
+ "0xF9",
+ "0xFA",
+ "0xFB",
+ "0xFC",
+ "0xFD",
+ "0xFE",
+ "0xFF",
+};
+static_assert(sizeof(byte_strs)/sizeof(byte_strs[0]) == 0x100);
+
+const char *const_byte_str(uint8_t b) {
+ return byte_strs[b];
}
diff --git a/libmisc/map.c b/libmisc/map.c
new file mode 100644
index 0000000..d1b2a57
--- /dev/null
+++ b/libmisc/map.c
@@ -0,0 +1,230 @@
+/* libmisc/map.c - A map/dict data structure
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <libmisc/alloc.h>
+#include <libmisc/assert.h>
+#include <libmisc/hash.h>
+#include <libmisc/map.h>
+
+#define FLAG_ITER (UINT8_C(1)<<0)
+#define FLAG_DEL (UINT8_C(1)<<1)
+
+/* Internal utilities *********************************************************/
+
+struct _map_kv {
+ uint8_t flags;
+ /* opaque key; */
+ /* opaque val; */
+};
+DLIST_DECLARE_NODE(_map_kv_list, struct _map_kv);
+
+static inline void *_map_kv_keyp(struct _map *m, struct _map_kv_list_node *kv) {
+ assert(m);
+ assert(kv);
+ return ((void*)kv)+m->offsetof_k;
+}
+static inline void *_map_kv_valp(struct _map *m, struct _map_kv_list_node *kv) {
+ assert(m);
+ assert(kv);
+ return ((void*)kv)+m->offsetof_v;
+}
+
+static inline void _map_lookup(struct _map *m, void *keyp,
+ hash_t *ret_hash,
+ struct _map_kv_list **ret_bucket,
+ struct _map_kv_list_node **ret_kv) {
+ assert(m);
+ assert(keyp);
+ assert(ret_hash);
+ assert(ret_bucket);
+ assert(ret_kv);
+ *ret_hash = hash(keyp, m->sizeof_k);
+ if (m->nbuckets == 0) {
+ *ret_bucket = NULL;
+ *ret_kv = NULL;
+ return;
+ }
+ *ret_bucket = &m->buckets[*ret_hash % m->nbuckets];
+ for (struct _map_kv_list_node *kv = (*ret_bucket)->front; kv; kv = kv->rear) {
+ if (!(kv->val.flags & FLAG_DEL) &&
+ memcmp(_map_kv_keyp(m, kv), keyp, m->sizeof_k) == 0) {
+ *ret_kv = kv;
+ return;
+ }
+ }
+ *ret_kv = NULL;
+}
+
+static inline void _map_resize(struct _map *m, size_t new_nbuckets) {
+ assert(m);
+ assert(new_nbuckets);
+ struct _map_kv_list *new_buckets = heap_alloc(new_nbuckets, struct _map_kv_list);
+ for (size_t i = 0; i < m->nbuckets; i++) {
+ while (m->buckets[i].front) {
+ struct _map_kv_list_node *kv = m->buckets[i].front;
+ dlist_pop_from_front(&m->buckets[i]);
+ hash_t h = hash(_map_kv_keyp(m, kv), m->sizeof_k);
+ dlist_push_to_rear(&new_buckets[h % new_nbuckets], kv);
+ }
+ }
+ m->nbuckets = new_nbuckets;
+ free(m->buckets);
+ m->buckets = new_buckets;
+}
+
+static bool _map_autoresize(struct _map *m) {
+ assert(m);
+ if (m->len > (m->nbuckets * 8/10)) {
+ size_t nbuckets = 1;
+ while (m->len > (nbuckets * 8/10))
+ nbuckets <<= 1;
+ _map_resize(m, nbuckets);
+ return true;
+ }
+ return false;
+}
+
+/* Methods ********************************************************************/
+
+void *_map_load(struct _map *m, void *keyp) {
+ assert(m);
+ assert(keyp);
+
+ hash_t h;
+ struct _map_kv_list *bucket;
+ struct _map_kv_list_node *kv;
+ _map_lookup(m, keyp, &h, &bucket, &kv);
+
+ if (!kv)
+ return NULL;
+ return _map_kv_valp(m, kv);
+}
+
+bool _map_del(struct _map *m, void *keyp) {
+ assert(m);
+ assert(keyp);
+
+ hash_t h;
+ struct _map_kv_list *bucket;
+ struct _map_kv_list_node *kv;
+ _map_lookup(m, keyp, &h, &bucket, &kv);
+
+ if (!kv)
+ return false;
+ if (kv->val.flags & FLAG_ITER) {
+ kv->val.flags |= FLAG_DEL;
+ } else {
+ dlist_remove(bucket, kv);
+ free(kv);
+ }
+ m->len--;
+ return true;
+}
+
+void *_map_store(struct _map *m, void *keyp, void *valp) {
+ assert(m);
+ assert(keyp);
+ assert(valp);
+
+ hash_t h;
+ struct _map_kv_list *bucket;
+ struct _map_kv_list_node *old;
+ _map_lookup(m, keyp, &h, &bucket, &old);
+
+ if (old) {
+ dlist_remove(bucket, old);
+ free(old);
+ m->len--;
+ }
+ m->len++;
+ if (!m->iterating && _map_autoresize(m)) {
+ h = hash(keyp, m->sizeof_k);
+ bucket = &m->buckets[h % m->nbuckets];
+ }
+ struct _map_kv_list_node *kv = calloc(1, m->sizeof_kv);
+ memcpy(_map_kv_keyp(m, kv), keyp, m->sizeof_k);
+ memcpy(_map_kv_valp(m, kv), valp, m->sizeof_v);
+ dlist_push_to_rear(bucket, kv);
+ return _map_kv_valp(m, kv);
+}
+
+void _map_free(struct _map *m) {
+ assert(m);
+
+ for (size_t i = 0; i < m->nbuckets; i++) {
+ while (m->buckets[i].front) {
+ struct _map_kv_list_node *kv = m->buckets[i].front;
+ dlist_pop_from_front(&m->buckets[i]);
+ free(kv);
+ }
+ }
+ free(m->buckets);
+ m->len = 0;
+ m->nbuckets = 0;
+ m->buckets = NULL;
+}
+
+/* Iteration ******************************************************************/
+
+struct _map_iter _map_iter_before(struct _map *m, void *keyp, void **valpp) {
+ assert(m);
+ assert(keyp);
+ assert(valpp);
+
+ struct _map_iter state = {
+ .m = m,
+ .keyp = keyp,
+ .valpp = valpp,
+ };
+ m->iterating++;
+ return state;
+}
+
+void _map_iter_after(struct _map_iter *state) {
+ assert(state);
+ assert(state->m);
+
+ state->m->iterating--;
+ if (!state->m->iterating)
+ _map_autoresize(state->m);
+}
+
+bool _map_iter_next(struct _map_iter *state) {
+ assert(state);
+ assert(state->m);
+ assert(state->valpp);
+
+ if (!state->kv) {
+ if (!state->m->len)
+ return false;
+ while (!state->m->buckets[state->i].front)
+ state->i++;
+ state->kv = state->m->buckets[state->i].front;
+ } else {
+ struct _map_kv_list_node *old_kv = state->kv;
+ state->kv = old_kv->rear;
+
+ old_kv->val.flags &= ~FLAG_ITER;
+ if (old_kv->val.flags & FLAG_DEL) {
+ dlist_remove(&state->m->buckets[state->i], old_kv);
+ free(old_kv);
+ }
+
+ while (!state->kv) {
+ state->i++;
+ if (state->i == state->m->nbuckets)
+ return false;
+ state->kv = state->m->buckets[state->i].front;
+ }
+ }
+ state->kv->val.flags |= FLAG_ITER;
+ memcpy(state->keyp, _map_kv_keyp(state->m, state->kv), state->m->sizeof_k);
+ *(state->valpp) = _map_kv_valp(state->m, state->kv);
+ return true;
+}
diff --git a/libmisc/rand.c b/libmisc/rand.c
new file mode 100644
index 0000000..d1643ee
--- /dev/null
+++ b/libmisc/rand.c
@@ -0,0 +1,38 @@
+/* libmisc/rand.c - Non-crytpographic random-number utilities
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdlib.h> /* for random() */
+
+#include <libmisc/assert.h>
+
+#include <libmisc/rand.h>
+
+uint64_t rand_uint63n(uint64_t cnt) {
+ assert(cnt != 0 && ((cnt-1) & 0x8000000000000000) == 0);
+ if (cnt <= UINT64_C(1)<<31) {
+ uint32_t fair_cnt = ((UINT32_C(1)<<31) / cnt) * cnt;
+ uint32_t rnd;
+ do {
+ rnd = random();
+ } while (rnd >= fair_cnt);
+ return rnd % cnt;
+ } else if (cnt <= UINT64_C(1)<<62) {
+ uint64_t fair_cnt = ((UINT64_C(1)<<62) / cnt) * cnt;
+ uint64_t rnd;
+ do {
+ rnd = (((uint64_t)random()) << 31) | random();
+ } while (rnd >= fair_cnt);
+ return rnd % cnt;
+ } else if (cnt <= UINT64_C(1)<<63) {
+ uint64_t fair_cnt = ((UINT64_C(1)<<63) / cnt) * cnt;
+ uint64_t rnd;
+ do {
+ rnd = (((uint64_t)random()) << 62) | (((uint64_t)random()) << 31) | random();
+ } while (rnd >= fair_cnt);
+ return rnd % cnt;
+ }
+ assert_notreached("cnt is out of bounds");
+}
diff --git a/libmisc/tests/test_assert.c b/libmisc/tests/test_assert.c
index 5b28561..290b073 100644
--- a/libmisc/tests/test_assert.c
+++ b/libmisc/tests/test_assert.c
@@ -1,45 +1,54 @@
/* libmisc/tests/test_assert.c - Tests for <libmisc/assert.h>
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <setjmp.h>
-#include <stdbool.h>
-#include <string.h>
#include <stdlib.h>
+#include <string.h>
+#include <libmisc/_intercept.h>
#include <libmisc/assert.h>
+#include <libmisc/fmt.h>
+#include <libmisc/macro.h>
#include "test.h"
-#define UNUSED(name)
-
/* Intercept failures and logging *********************************************/
-bool global_failed;
-char *global_log;
-jmp_buf global_env;
+static bool global_failed;
+static struct fmt_buf global_log;
+static jmp_buf global_env;
#define with_intercept() ({ \
global_failed = false; \
- if (global_log) \
- free(global_log); \
- global_log = NULL; \
+ global_log_clear(); \
setjmp(global_env) == 0; \
})
-[[noreturn]] void abort(void) {
+void __lm_abort(void) {
global_failed = true;
longjmp(global_env, 1);
}
-#define __builtin_unreachable() test_assert(0)
+void __lm_putchar(unsigned char c) {
+ if (global_log.len+1 >= global_log.cap) {
+ global_log.cap += 16;
+ global_log.dat = realloc(global_log.dat, global_log.cap);
+ memset(global_log.dat + global_log.len, 0, global_log.cap - global_log.len);
+ }
+ ((uint8_t *)global_log.dat)[global_log.len++] = (uint8_t)c;
+}
-int vprintf(const char *format, va_list ap) {
- return vasprintf(&global_log, format, ap);
+static void global_log_clear(void) {
+ if (global_log.dat)
+ memset(global_log.dat, 0, global_log.cap);
+ global_log.len = 0;
}
+#define __builtin_unreachable() test_assert(0)
+
/* Utilities ******************************************************************/
#define test_should_succeed(test) do { \
@@ -47,44 +56,45 @@ int vprintf(const char *format, va_list ap) {
test; \
} \
test_assert(global_failed == false); \
- test_assert(global_log == NULL); \
+ test_assert(global_log.len == 0); \
} while (0)
-#define test_should_fail(test, exp_log) do { \
- if (with_intercept()) { \
- test; \
- } \
- test_assert(global_failed == true); \
- if (!(global_log != NULL && \
- strcmp(global_log, exp_log) == 0)) { \
- printf("exp = \"%s\"\n" \
- "act = \"%s\"\n", \
- exp_log, global_log); \
- test_assert(0); \
- } \
+#define test_should_fail(test, exp_log) do { \
+ if (with_intercept()) { \
+ test; \
+ } \
+ test_assert(global_failed == true); \
+ if (!(global_log.len != 0 && \
+ strcmp(global_log.dat, exp_log) == 0)) { \
+ printf("exp = \"%s\"\n" \
+ "act = \"%s\"\n", \
+ exp_log, (char *)global_log.dat); \
+ test_assert(0); \
+ } \
} while (0)
-#define _STR(x) #x
-#define STR(x) _STR(x)
-
/* Actual tests ***************************************************************/
static_assert(sizeof(char) == 1);
int main() {
+#ifndef NDEBUG
test_should_succeed(assert(true));
- test_should_fail(assert(false), "error: ASSERT: "__FILE__":"STR(__LINE__)":main(): assertion \"false\" failed\n");
+ test_should_fail(assert(false), "error: ASSERT: "__FILE__":"LM_STR_(__LINE__)":main(): assertion \"false\" failed\n");
test_should_succeed(assert_msg(true, "foo"));
- test_should_fail(assert_msg(false, "foo"), "error: ASSERT: "__FILE__":"STR(__LINE__)":main(): assertion \"false\" failed: foo\n");
+ test_should_fail(assert_msg(false, "foo"), "error: ASSERT: "__FILE__":"LM_STR_(__LINE__)":main(): assertion \"false\" failed: foo\n");
test_should_succeed(assert_msg(true, NULL));
- test_should_fail(assert_msg(false, NULL), "error: ASSERT: "__FILE__":"STR(__LINE__)":main(): assertion \"false\" failed\n");
+ test_should_fail(assert_msg(false, NULL), "error: ASSERT: "__FILE__":"LM_STR_(__LINE__)":main(): assertion \"false\" failed\n");
- test_should_fail(assert_notreached("xxx"), "error: ASSERT: "__FILE__":"STR(__LINE__)":main(): assertion \"notreached\" failed: xxx\n");
+ test_should_fail(assert_notreached("xxx"), "error: ASSERT: "__FILE__":"LM_STR_(__LINE__)":main(): assertion \"notreached\" failed: xxx\n");
+#endif
- if (global_log) {
- free(global_log);
- global_log = NULL;
+ if (global_log.dat) {
+ global_log_clear();
+ free(global_log.dat);
+ global_log.dat = NULL;
+ global_log.cap = 0;
}
return 0;
}
diff --git a/libmisc/tests/test_assert_min.c b/libmisc/tests/test_assert_min.c
new file mode 100644
index 0000000..9c0394b
--- /dev/null
+++ b/libmisc/tests/test_assert_min.c
@@ -0,0 +1,17 @@
+/* libmisc/tests/test_assert_min.c - Tests for minimal <libmisc/assert.h>
+ *
+ * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* Don't include *anything* else. */
+#include <libmisc/assert.h>
+
+static_assert(1 == 1);
+
+int main() {
+ assert_msg(1, "foo");
+ assert(1);
+ return 0;
+ assert_notreached("ret");
+}
diff --git a/libmisc/tests/test_endian.c b/libmisc/tests/test_endian.c
index d0b547c..8c48727 100644
--- a/libmisc/tests/test_endian.c
+++ b/libmisc/tests/test_endian.c
@@ -1,6 +1,6 @@
/* libmisc/tests/test_endian.c - Tests for <libmisc/endian.h>
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -11,22 +11,31 @@
#include "test.h"
int main() {
- uint8_t act[12] = {0};
- uint16be_encode(&act[0], UINT16_C(0x1234));
- uint32be_encode(&act[2], UINT32_C(0x56789ABC));
- uint16le_encode(&act[6], UINT16_C(0x1234));
- uint32le_encode(&act[8], UINT32_C(0x56789ABC));
+ uint8_t act[(2+4+8)*2] = {};
+ size_t pos = 0;
+ pos += uint16be_encode(&act[pos], UINT16_C(0x1234));
+ pos += uint32be_encode(&act[pos], UINT32_C(0x56789ABC));
+ pos += uint64be_encode(&act[pos], UINT64_C(0xAC589A93278CB30A));
+ pos += uint16le_encode(&act[pos], UINT16_C(0x1234));
+ pos += uint32le_encode(&act[pos], UINT32_C(0x56789ABC));
+ pos += uint64le_encode(&act[pos], UINT64_C(0xAC589A93278CB30A));
- uint8_t exp[12] = { 0x12, 0x34,
- 0x56, 0x78, 0x9A, 0xBC,
- 0x34, 0x12,
- 0xBC, 0x9A, 0x78, 0x56 };
- test_assert(memcmp(act, exp, 12) == 0);
+ test_assert(pos == sizeof(act));
+ uint8_t exp[(2+4+8)*2] = { 0x12, 0x34,
+ 0x56, 0x78, 0x9A, 0xBC,
+ 0xAC, 0x58, 0x9A, 0x93, 0x27, 0x8C, 0xB3, 0x0A,
+ 0x34, 0x12,
+ 0xBC, 0x9A, 0x78, 0x56,
+ 0x0A, 0xB3, 0x8C, 0x27, 0x93, 0x9A, 0x58, 0xAC};
+ test_assert(memcmp(act, exp, sizeof(act)) == 0);
- test_assert(uint16be_decode(&act[0]) == UINT16_C(0x1234));
- test_assert(uint32be_decode(&act[2]) == UINT32_C(0x56789ABC));
- test_assert(uint16le_decode(&act[6]) == UINT16_C(0x1234));
- test_assert(uint32le_decode(&act[8]) == UINT32_C(0x56789ABC));
+ pos = 0;
+ test_assert(uint16be_decode(&act[pos]) == UINT16_C(0x1234)); pos += 2;
+ test_assert(uint32be_decode(&act[pos]) == UINT32_C(0x56789ABC)); pos += 4;
+ test_assert(uint64be_decode(&act[pos]) == UINT64_C(0xAC589A93278CB30A)); pos += 8;
+ test_assert(uint16le_decode(&act[pos]) == UINT16_C(0x1234)); pos += 2;
+ test_assert(uint32le_decode(&act[pos]) == UINT32_C(0x56789ABC)); pos += 4;
+ test_assert(uint64le_decode(&act[pos]) == UINT64_C(0xAC589A93278CB30A)); pos += 8;
return 0;
}
diff --git a/libmisc/tests/test_fmt.c b/libmisc/tests/test_fmt.c
new file mode 100644
index 0000000..64b3b8a
--- /dev/null
+++ b/libmisc/tests/test_fmt.c
@@ -0,0 +1,243 @@
+/* libmisc/tests/test_fmt.c - Tests for <libmisc/fmt.h>
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdlib.h> /* for free() */
+#include <string.h> /* for strcmp(), memcmp(), memset() */
+
+#include <libmisc/fmt.h>
+
+#include "test.h"
+
+int main() {
+ char str[128] = {};
+#define do_print(...) fmt_snprint(str, sizeof(str), __VA_ARGS__)
+
+ do_print("hello ", 9, " world!\n");
+ test_assert(strcmp(str, "hello 9 world!\n") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print("hello ", (base8, 9), " world!\n");
+ test_assert(strcmp(str, "hello 11 world!\n") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print("hello ", (base2, 9), (qstr, " world!\n"));
+ test_assert(strcmp(str, "hello 1001\" world!\\n\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print("hello ", (base16, 17), " world!\n");
+ test_assert(strcmp(str, "hello 11 world!\n") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((strn, "hello ", 4));
+ test_assert(strcmp(str, "hell") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((strn, "h\0ello ", 4));
+ test_assert(memcmp(str, "h\0\0", 3) == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((mem, "hello ", 4));
+ test_assert(strcmp(str, "hell") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((mem, "hello\0world", strlen("hello world")+1));
+ test_assert(memcmp(str, "hello\0world", strlen("hello world")+1) == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qmem, "hello\0world", strlen("hello world")+1));
+ test_assert(strcmp(str, "\"hello\\0world\\0\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qstr, "hello\0world"));
+ test_assert(strcmp(str, "\"hello\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qstrn, "hello\0world", strlen("hello world")+1));
+ test_assert(strcmp(str, "\"hello\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qstrn, "hello\0world", 4));
+ test_assert(strcmp(str, "\"hell\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qstr, "hello\xFFworld🚧"));
+ test_assert(strcmp(str, "\"hello\\xFFworld\\U0001F6A7\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qstr, "¡hello world!"));
+ test_assert(strcmp(str, "\"\\u00A1hello world!\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qmem, "🚧", 3)); /* truncated UTF-8 */
+ test_assert(strcmp(str, "\"\\xF0\\x9F\\x9A\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qmem, "\xF7\xBF\xBF\xBF", 4)); /* over unicode_max */
+ test_assert(strcmp(str, "\"\\xF7\\xBF\\xBF\\xBF\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qmem, "\xE0\xA0", 2)); /* non-optimal encoding (of ' ') */
+ test_assert(strcmp(str, "\"\\xE0\\xA0\"") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((byte, 'h'), (byte, 'w'));
+ test_assert(strcmp(str, "hw") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, 'h'), (qbyte, 'w'));
+ test_assert(strcmp(str, "'h''w'") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, 0));
+ test_assert(strcmp(str, "'\\0'") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, '\\'));
+ test_assert(strcmp(str, "'\\\\'") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, '\''));
+ test_assert(strcmp(str, "'\\''") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, '\n'));
+ test_assert(strcmp(str, "'\\n'") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((qbyte, 0xff));
+ test_assert(strcmp(str, "'\\xFF'") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print("zero ", 0);
+ test_assert(strcmp(str, "zero 0") == 0);
+ memset(str, 0, sizeof(str));
+
+ const char *const_str = "hello";
+ do_print(const_str);
+ test_assert(strcmp(str, "hello") == 0);
+ memset(str, 0, sizeof(str));
+
+ bool t = true;
+ do_print(t);
+ test_assert(strcmp(str, "true") == 0);
+ memset(str, 0, sizeof(str));
+
+ bool f = false;
+ do_print(f);
+ test_assert(strcmp(str, "false") == 0);
+ memset(str, 0, sizeof(str));
+
+ /* Check that it accepts all primitive types of integer, not
+ * just all sizes of integer (e.g., on x86-64,
+ * sizeof(long)==sizeof(int), but they're different primitive
+ * types). */
+ {
+ signed char x = 42;
+ do_print("schar ", x);
+ test_assert(strcmp(str, "schar 42") == 0);
+ memset(str, 0, sizeof(str));
+ }
+ {
+ unsigned char x = 43;
+ do_print("uchar ", x);
+ test_assert(strcmp(str, "uchar 43") == 0);
+ memset(str, 0, sizeof(str));
+ }
+
+ {
+ short x = 44;
+ do_print("short ", x);
+ test_assert(strcmp(str, "short 44") == 0);
+ memset(str, 0, sizeof(str));
+ }
+ {
+ unsigned short x = 45;
+ do_print("ushort ", x);
+ test_assert(strcmp(str, "ushort 45") == 0);
+ memset(str, 0, sizeof(str));
+ }
+
+ {
+ int x = 46;
+ do_print("int ", x);
+ test_assert(strcmp(str, "int 46") == 0);
+ memset(str, 0, sizeof(str));
+ }
+ {
+ unsigned int x = 47;
+ do_print("uint ", x);
+ test_assert(strcmp(str, "uint 47") == 0);
+ memset(str, 0, sizeof(str));
+ }
+
+ {
+ long x = 48;
+ do_print("long ", x);
+ test_assert(strcmp(str, "long 48") == 0);
+ memset(str, 0, sizeof(str));
+ }
+ {
+ unsigned long x = 49;
+ do_print("ulong ", x);
+ test_assert(strcmp(str, "ulong 49") == 0);
+ memset(str, 0, sizeof(str));
+ }
+
+ {
+ long long x = 50;
+ do_print("long long ", x);
+ test_assert(strcmp(str, "long long 50") == 0);
+ memset(str, 0, sizeof(str));
+ }
+ {
+ unsigned long long x = 51;
+ do_print("ulong long ", x);
+ test_assert(strcmp(str, "ulong long 51") == 0);
+ memset(str, 0, sizeof(str));
+ }
+
+ do_print((ljust, 10, ' ', (base10, 1), "x"));
+ test_assert(strcmp(str, "1x ") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((rjust, 10, ' ', (base10, 1), "x"));
+ test_assert(strcmp(str, " 1x") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((base16_u8_, 1));
+ test_assert(strcmp(str, "0x01") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((base16_u16_, 1));
+ test_assert(strcmp(str, "0x0001") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((base16_u32_, 1));
+ test_assert(strcmp(str, "0x00000001") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((base16_u64_, 1));
+ test_assert(strcmp(str, "0x0000000000000001") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((hbyte, 1));
+ test_assert(strcmp(str, "0x01") == 0);
+ memset(str, 0, sizeof(str));
+
+ do_print((hmem, "hello", 6));
+ test_assert(strcmp(str, "{0x68,0x65,0x6C,0x6C,0x6F,0x00}") == 0);
+ memset(str, 0, sizeof(str));
+
+ char *astr = fmt_asprint("");
+ test_assert(astr != NULL && astr[0] == '\0');
+ free(astr);
+
+ astr = fmt_asprint("hello ", (base2, 9), (qstr, " world!\n"));
+ test_assert(strcmp(astr, "hello 1001\" world!\\n\"") == 0);
+ free(astr);
+
+ return 0;
+}
diff --git a/libmisc/tests/test_log.c b/libmisc/tests/test_log.c
index 286738d..6e7cdfd 100644
--- a/libmisc/tests/test_log.c
+++ b/libmisc/tests/test_log.c
@@ -1,61 +1,80 @@
/* libmisc/tests/test_log.c - Tests for <libmisc/log.h>
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-#define _GNU_SOURCE /* for vasprintf() */
-#include <stdarg.h> /* for va_list */
-#include <stdio.h> /* for vasprintf() */
-#include <stdlib.h> /* for free() */
-#include <string.h> /* for strcmp() */
+#include <stdio.h> /* for vsnprintf() */
+#include <stdlib.h> /* for realloc(), free() */
+#include <string.h> /* for strlen(), strcmp() */
#define LOG_NAME FROBNICATE
#include <libmisc/log.h>
+#include <libmisc/_intercept.h>
+
#include "test.h"
/* Intercept output ***********************************************************/
-static char *log_output = NULL;
+static struct fmt_buf log_output = {};
+
+void __lm_putchar(unsigned char c) {
+ if (log_output.len+1 >= log_output.cap) {
+ log_output.cap += 16;
+ log_output.dat = realloc(log_output.dat, log_output.cap);
+ memset(log_output.dat + log_output.len, 0, log_output.cap - log_output.len);
+ }
+ ((uint8_t *)log_output.dat)[log_output.len++] = (uint8_t)c;
+}
-int vprintf(const char *format, va_list ap) {
- return vasprintf(&log_output, format, ap);
+static void log_output_clear(void) {
+ if (log_output.dat)
+ memset(log_output.dat, 0, log_output.cap);
+ log_output.len = 0;
}
/* Actual tests ***************************************************************/
-#define should_print(_exp, cmd) do { \
- char *exp = _exp; \
- test_assert(!log_output); \
- cmd; \
- if (!exp) \
- test_assert(!log_output); \
- else \
- if (!(log_output != NULL && \
- strcmp(log_output, exp) == 0)) { \
- printf("exp = \"%s\"\n" \
- "act = \"%s\"\n", \
- exp, log_output); \
- test_assert(0); \
- } \
- if (log_output) { \
- free(log_output); \
- log_output = NULL; \
- } \
+#define should_print(_exp, cmd) do { \
+ char *exp = _exp; \
+ test_assert(log_output.len == 0); \
+ cmd; \
+ if (!exp) \
+ test_assert(log_output.len == 0); \
+ else { \
+ test_assert(log_output.dat); \
+ test_assert(strlen(log_output.dat) == log_output.len); \
+ if (strcmp(log_output.dat, exp)) { \
+ printf("exp = \"%s\"\n" \
+ "act = \"%s\"\n", \
+ exp, (char *)log_output.dat); \
+ test_assert(0); \
+ } \
+ } \
+ log_output_clear(); \
} while (0)
int main() {
should_print("error: FROBNICATE: val=42\n",
- errorf("val=%d", 42));
+ log_errorln("val=", 42));
should_print("info : FROBNICATE: val=0\n",
- infof("val=%d", 0));
+ log_infoln("val=", 0));
+#ifndef NDEBUG
#define CONFIG_FROBNICATE_DEBUG 1
should_print("debug: FROBNICATE: val=-2\n",
- debugf("val=%d", -2));
+ log_debugln("val=", -2));
#undef CONFIG_FROBNICATE_DEBUG
#define CONFIG_FROBNICATE_DEBUG 0
should_print(NULL,
- debugf("val=%d", -2));
+ log_debugln("val=", -2));
+#undef CONFIG_FROBNICATE_DEBUG
+#endif
+
+ if (log_output.dat) {
+ free(log_output.dat);
+ log_output.dat = NULL;
+ log_output.cap = 0;
+ }
return 0;
}
diff --git a/libmisc/tests/test_macro.c b/libmisc/tests/test_macro.c
new file mode 100644
index 0000000..6810005
--- /dev/null
+++ b/libmisc/tests/test_macro.c
@@ -0,0 +1,191 @@
+/* libmisc/tests/test_macro.c - Tests for <libmisc/macro.h>
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdlib.h> /* for free() */
+#include <string.h> /* for strcmp(), strlen(), memcmp(), strdup() */
+
+#include <libmisc/macro.h>
+
+#include "test.h"
+
+/** Given `N` from `#define _LM_EVAL _LM_EVAL__{N}`, UNDER is `(N*2)-2`. (16*2)-2=30. */
+#define UNDER 30
+/** Given `N` from `#define _LM_EVAL _LM_EVAL__{N}`, OVER is `(N*2)-1`. (16*2)-1=31. */
+#define OVER 31
+
+/** XUNDER is 0 through `UNDER` inclusive. */
+#define XUNDER \
+ X(0) X(1) X(2) X(3) X(4) X(5) X(6) X(7) X(8) X(9) X(10) X(11) X(12) X(13) X(14) X(15) \
+ X(16) X(17) X(18) X(19) X(20) X(21) X(22) X(23) X(24) X(25) X(26) X(27) X(28) X(29) X(30)
+/** XUNDER is 0 through `OVER` inclusive. */
+#define XOVER XUNDER X(OVER)
+
+static char *without_spaces(const char *in) {
+ char *out = strdup(in);
+ for (size_t i = 0; out[i]; i++)
+ while (out[i] == ' ')
+ for (size_t j = i; out[j]; j++)
+ out[j] = out[j+1];
+ return out;
+}
+
+int main() {
+ printf("== LM_NEXT_POWER_OF_2 =====================================\n");
+ /* valid down to 0. */
+ test_assert(LM_NEXT_POWER_OF_2(0) == 1);
+ test_assert(LM_NEXT_POWER_OF_2(1) == 2);
+ test_assert(LM_NEXT_POWER_OF_2(2) == 4);
+ test_assert(LM_NEXT_POWER_OF_2(3) == 4);
+ test_assert(LM_NEXT_POWER_OF_2(4) == 8);
+ test_assert(LM_NEXT_POWER_OF_2(5) == 8);
+ test_assert(LM_NEXT_POWER_OF_2(6) == 8);
+ test_assert(LM_NEXT_POWER_OF_2(7) == 8);
+ test_assert(LM_NEXT_POWER_OF_2(8) == 16);
+ /* ... */
+ test_assert(LM_NEXT_POWER_OF_2(16) == 32);
+ /* ... */
+ test_assert(LM_NEXT_POWER_OF_2(0x7000000000000000) == 0x8000000000000000);
+ /* ... */
+ test_assert(LM_NEXT_POWER_OF_2(0x8000000000000000-1) == 0x8000000000000000);
+ /* Valid up to 0x8000000000000000-1 = (1<<63)-1 */
+
+ printf("== LM_FLOORLOG2 ===========================================\n");
+ /* valid down to 1. */
+ test_assert(LM_FLOORLOG2(1) == 0);
+ test_assert(LM_FLOORLOG2(2) == 1);
+ test_assert(LM_FLOORLOG2(3) == 1);
+ test_assert(LM_FLOORLOG2(4) == 2);
+ test_assert(LM_FLOORLOG2(5) == 2);
+ test_assert(LM_FLOORLOG2(6) == 2);
+ test_assert(LM_FLOORLOG2(7) == 2);
+ test_assert(LM_FLOORLOG2(8) == 3);
+ /* ... */
+ test_assert(LM_FLOORLOG2(16) == 4);
+ /* ... */
+ test_assert(LM_FLOORLOG2(0x80000000) == 31);
+ /* ... */
+ test_assert(LM_FLOORLOG2(0xFFFFFFFF) == 31);
+ test_assert(LM_FLOORLOG2(0x100000000) == 32);
+ /* ... */
+ test_assert(LM_FLOORLOG2(0x8000000000000000) == 63);
+ /* ... */
+ test_assert(LM_FLOORLOG2(0xFFFFFFFFFFFFFFFF) == 63);
+
+ printf("== LM_TUPLE ===============================================\n");
+ test_assert(LM_IF(LM_IS_TUPLE( 9 ))(0)(1));
+ test_assert(LM_IF(LM_IS_TUPLE( a ))(0)(1));
+ test_assert(LM_IF(LM_IS_TUPLE( () ))(1)(0));
+ test_assert(LM_IF(LM_IS_TUPLE( (9) ))(1)(0));
+ test_assert(LM_IF(LM_IS_TUPLE( (a) ))(1)(0));
+ test_assert(LM_IF(LM_IS_TUPLE( (a, b) ))(1)(0));
+
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( () ))(1)(0));
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( 9 ))(0)(1));
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( a ))(0)(1));
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( (9) ))(0)(1));
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( (a) ))(0)(1));
+ test_assert(LM_IF(LM_IS_EMPTY_TUPLE( (a, b) ))(0)(1));
+
+ printf("== LM_TUPLES ==============================================\n");
+ test_assert(LM_IF(LM_TUPLES_IS_NONEMPTY( ))(0)(1));
+ test_assert(LM_IF(LM_TUPLES_IS_NONEMPTY( () ))(1)(0));
+ test_assert(LM_IF(LM_TUPLES_IS_NONEMPTY( (a) ))(1)(0));
+ test_assert(LM_IF(LM_TUPLES_IS_NONEMPTY( (a)(b) ))(1)(0));
+ test_assert(LM_IF(LM_TUPLES_IS_NONEMPTY( (a)(b)(c) ))(1)(0));
+
+ printf("== LM_FOREACH_PARAM =======================================\n");
+ /* Basic test. */
+ {
+ #define FN(A, B) A "-" #B
+ const char *str = LM_FOREACH_PARAM(FN, (" "), a, (b), c);
+ #undef FN
+ test_assert(strcmp(str, " -a -(b) -c") == 0);
+ }
+
+ /* Test that it works with the documented limit of params. */
+ {
+ #define X(n) , n
+ #define FN(n) #n "\n"
+ const char *str = LM_FOREACH_PARAM_(FN, () XUNDER);
+ #undef FN
+ #undef X
+ #define X(n) #n "\n"
+ test_assert(strcmp(str, XUNDER) == 0);
+ #undef X
+ }
+
+ /* Test that it breaks at documented_limit+1 tuples. */
+ {
+ #define X(n) , n
+ #define FN(n) n
+ const char *str = LM_STR_(LM_FOREACH_PARAM_(FN, () XOVER));
+ #undef FN
+ #undef X
+ /* This comparison is a little extra complicated in
+ * order to not be sensitive to whitespace in the
+ * suffix. */
+ #define X(n) #n " "
+ const char *exp_prefix = XUNDER;
+ #undef X
+ const char *exp_suffix = "FN(" LM_STR_(OVER) ")_LM_FOREACH_PARAM_ITEM_indirect()(FN,(),())";
+ test_assert(strlen(exp_prefix) < strlen(str) && memcmp(exp_prefix, str, strlen(exp_prefix)) == 0);
+ char *act_suffix = without_spaces(&str[strlen(exp_prefix)]);
+ test_assert(strcmp(act_suffix, exp_suffix) == 0);
+ free(act_suffix);
+ }
+
+ printf("== LM_FOREACH_TUPLE =======================================\n");
+ /* Basic test. */
+ {
+ #define FN(a, b) a "-" b
+ const char *str = LM_FOREACH_TUPLE( ("a") ("b") ("c"), FN, " ");
+ #undef FN
+ test_assert(strcmp(str, " -a -b -c") == 0);
+ }
+
+ /* Test that it works with the documented limit of tuples. */
+ {
+ #define X(n) (n)
+ #define FN(n) #n "\n"
+ const char *str = LM_FOREACH_TUPLE(XUNDER, FN);
+ #undef FN
+ #undef X
+ #define X(n) #n "\n"
+ test_assert(strcmp(str, XUNDER) == 0);
+ #undef X
+ }
+
+ /* Test that it breaks at documented_limit+1 tuples. */
+ {
+ #define X(n) (n)
+ #define FN(n) n
+ const char *str = LM_STR_(LM_FOREACH_TUPLE(XOVER, FN));
+ #undef FN
+ #undef X
+ /* This comparison is a little extra complicated in
+ * order to not be sensitive to whitespace in the
+ * suffix. */
+ #define X(n) #n " "
+ const char *exp_prefix = XUNDER;
+ #undef X
+ const char *exp_suffix = "FN(" LM_STR_(OVER) ")_LM_FOREACH_TUPLE_indirect()(,FN,)";
+ test_assert(strlen(exp_prefix) < strlen(str) && memcmp(exp_prefix, str, strlen(exp_prefix)) == 0);
+ char *act_suffix = without_spaces(&str[strlen(exp_prefix)]);
+ test_assert(strcmp(act_suffix, exp_suffix) == 0);
+ free(act_suffix);
+ }
+
+ printf("== LM_DEFAPPEND ===========================================\n");
+ LM_DEFAPPEND(mylist, a);
+ LM_DEFAPPEND(mylist,
+ b);
+ {
+ const char *str = LM_STR_(mylist);
+ test_assert(strcmp(str, "a b") == 0);
+ }
+
+ return 0;
+}
diff --git a/libmisc/tests/test_map.c b/libmisc/tests/test_map.c
new file mode 100644
index 0000000..855dace
--- /dev/null
+++ b/libmisc/tests/test_map.c
@@ -0,0 +1,60 @@
+/* libmisc/tests/test_map.c - Tests for <libmisc/map.h>
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/map.h>
+
+#include "test.h"
+
+MAP_DECLARE(intmap, int, int);
+
+int main() {
+ struct intmap m = {};
+ test_assert(map_len(&m) == 0);
+
+ int *v = map_store(&m, 3, 8);
+ test_assert(v && *v == 8);
+ test_assert(map_len(&m) == 1);
+
+ v = map_load(&m, 3);
+ test_assert(v);
+ test_assert(*v == 8);
+
+ v = NULL;
+
+ test_assert(map_del(&m, 3));
+ test_assert(map_len(&m) == 0);
+ test_assert(!map_del(&m, 3));
+
+ map_store(&m, 1, 11);
+ map_store(&m, 2, 12);
+ map_store(&m, 3, 13);
+ test_assert(map_len(&m) == 3);
+ bool seen_1 = false, seen_2 = false, seen_3 = false;
+ MAP_FOREACH(&m, ik, iv) {
+ switch (ik) {
+ case 1: seen_1 = true; break;
+ case 2: seen_2 = true; break;
+ case 3: seen_3 = true; break;
+ }
+ switch (ik) {
+ case 1: case 2: case 3:
+ map_store(&m, ik+20, (*iv)+20);
+ test_assert(map_del(&m, ik));
+ test_assert(!map_del(&m, ik));
+ test_assert(map_load(&m, ik) == NULL);
+ break;
+ }
+ }
+ test_assert(map_len(&m) == 3);
+ test_assert(seen_1); v = map_load(&m, 21); test_assert(v && *v == 31); v = map_load(&m, 1); test_assert(!v);
+ test_assert(seen_2); v = map_load(&m, 22); test_assert(v && *v == 32); v = map_load(&m, 2); test_assert(!v);
+ test_assert(seen_3); v = map_load(&m, 23); test_assert(v && *v == 33); v = map_load(&m, 3); test_assert(!v);
+
+ map_free(&m);
+ test_assert(map_len(&m) == 0);
+
+ return 0;
+}
diff --git a/libmisc/tests/test_obj.c b/libmisc/tests/test_obj.c
new file mode 100644
index 0000000..a13b8c9
--- /dev/null
+++ b/libmisc/tests/test_obj.c
@@ -0,0 +1,61 @@
+/* libmisc/tests/test_obj.c - Tests for <libmisc/obj.h>
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/obj.h>
+
+#include "test.h"
+
+/* `lo_inteface frobber` header ***********************************************/
+
+#define frobber_LO_IFACE \
+ /** Basic function. */ \
+ LO_FUNC(int, frob) \
+ /** Function that takes 1 argument. */ \
+ LO_FUNC(int, frob1, int) \
+ /** Function that returns nothing. */ \
+ LO_FUNC(void, frob0)
+LO_INTERFACE(frobber);
+
+/* `struct myclass` header ****************************************************/
+
+struct myclass {
+ int a;
+};
+LO_IMPLEMENTATION_H(frobber, struct myclass, myclass);
+
+/* `struct myclass` implementation ********************************************/
+
+LO_IMPLEMENTATION_C(frobber, struct myclass, myclass, static);
+
+static int myclass_frob(struct myclass *self) {
+ test_assert(self);
+ return self->a;
+}
+
+static int myclass_frob1(struct myclass *self, int arg) {
+ test_assert(self);
+ return arg;
+}
+
+static void myclass_frob0(struct myclass *self) {
+ test_assert(self);
+}
+
+/* main test body *************************************************************/
+
+#define MAGIC1 909837
+#define MAGIC2 657441
+
+int main() {
+ struct myclass obj = {
+ .a = MAGIC1,
+ };
+ lo_interface frobber iface = LO_BOX(frobber, &obj);
+ test_assert(LO_CALL(iface, frob) == MAGIC1);
+ test_assert(LO_CALL(iface, frob1, MAGIC2) == MAGIC2);
+ LO_CALL(iface, frob0);
+ return 0;
+}
diff --git a/libmisc/tests/test_obj_nest.c b/libmisc/tests/test_obj_nest.c
new file mode 100644
index 0000000..ba5ac37
--- /dev/null
+++ b/libmisc/tests/test_obj_nest.c
@@ -0,0 +1,73 @@
+/* libmisc/tests/test_obj_nest.c - Tests for <libmisc/obj.h> nesting
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <string.h> /* for memcpy() */
+
+#include <libmisc/obj.h>
+
+#include "test.h"
+
+/* interfaces *****************************************************************/
+
+#define reader_LO_IFACE \
+ LO_FUNC(ssize_t, read, void *, size_t)
+LO_INTERFACE(reader);
+
+#define writer_LO_IFACE \
+ LO_FUNC(ssize_t, write, void *, size_t)
+LO_INTERFACE(writer);
+
+#define read_writer_LO_IFACE \
+ LO_NEST(reader) \
+ LO_NEST(writer)
+LO_INTERFACE(read_writer);
+
+/* implementation header ******************************************************/
+
+struct myclass {
+ size_t len;
+ char buf[512];
+};
+LO_IMPLEMENTATION_H(reader, struct myclass, myclass);
+LO_IMPLEMENTATION_H(writer, struct myclass, myclass);
+LO_IMPLEMENTATION_H(read_writer, struct myclass, myclass);
+
+/* implementation main ********************************************************/
+
+LO_IMPLEMENTATION_C(reader, struct myclass, myclass, static);
+LO_IMPLEMENTATION_C(writer, struct myclass, myclass, static);
+LO_IMPLEMENTATION_C(read_writer, struct myclass, myclass, static);
+
+static ssize_t myclass_read(struct myclass *self, void *buf, size_t count) {
+ test_assert(self);
+ if (count > self->len)
+ count = self->len;
+ memcpy(buf, self->buf, count);
+ return count;
+}
+
+static ssize_t myclass_write(struct myclass *self, void *buf, size_t count) {
+ test_assert(self);
+ if (self->len)
+ return -1;
+ if (count > sizeof(self->buf))
+ count = sizeof(self->buf);
+ memcpy(self->buf, buf, count);
+ self->len = count;
+ return count;
+}
+
+/* main test body *************************************************************/
+
+int main() {
+ struct myclass _obj = {};
+ lo_interface read_writer obj = LO_BOX(read_writer, &_obj);
+ test_assert(LO_CALL(obj, write, "Hello", 6) == 6);
+ char buf[6] = {};
+ test_assert(LO_CALL(obj, read, buf, 3) == 3);
+ test_assert(memcmp(buf, "Hel\0\0\0", 6) == 0);
+ return 0;
+}
diff --git a/libmisc/tests/test_private.c b/libmisc/tests/test_private.c
index 7aaf1ee..024dddb 100644
--- a/libmisc/tests/test_private.c
+++ b/libmisc/tests/test_private.c
@@ -1,6 +1,6 @@
/* libmisc/tests/test_private.c - Tests for <libmisc/private.h>
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -8,18 +8,18 @@
struct a {
int foo;
- BEGIN_PRIVATE(A)
+ BEGIN_PRIVATE(A);
int bar;
- END_PRIVATE(A)
+ END_PRIVATE(A);
};
#define IMPLEMENTATION_FOR_B YES
struct b {
int foo;
- BEGIN_PRIVATE(B)
+ BEGIN_PRIVATE(B);
int bar;
- END_PRIVATE(B)
+ END_PRIVATE(B);
};
int main() {
diff --git a/libmisc/tests/test_rand.c b/libmisc/tests/test_rand.c
index fff1b27..1cfbd65 100644
--- a/libmisc/tests/test_rand.c
+++ b/libmisc/tests/test_rand.c
@@ -1,33 +1,24 @@
/* libmisc/tests/test_rand.c - Tests for <libmisc/rand.h>
*
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-#include <stdbool.h>
#include <setjmp.h>
+#include <libmisc/_intercept.h>
#include <libmisc/rand.h>
#include "test.h"
/* Intercept failures *********************************************************/
+#ifndef NDEBUG
jmp_buf *__catch;
-void __assert_msg_fail(const char *expr,
- const char *file, unsigned int line, const char *func,
- const char *msg) {
- static bool in_fail = false;
+void __lm_abort(void) {
if (__catch)
longjmp(*__catch, 1);
- if (!in_fail) {
- in_fail = true;
- printf("error: %s:%u:%s(): assertion \"%s\" failed%s%s\n",
- file, line, func,
- expr,
- msg ? ": " : "", msg);
- }
abort();
}
@@ -43,6 +34,7 @@ void __assert_msg_fail(const char *expr,
__catch = old_catch; \
} \
} while (0);
+#endif
/* Actual tests ***************************************************************/
@@ -51,20 +43,24 @@ void __assert_msg_fail(const char *expr,
static void test_n(uint64_t cnt) {
if (cnt == 0 || cnt > UINT64_C(1)<<63) {
+#ifndef NDEBUG
should_abort(rand_uint63n(cnt));
+#else
+ return;
+#endif
} else {
double sum = 0;
- bool seen[MAX_SEE_ALL] = {0};
+ bool seen[MAX_SEE_ALL] = {};
for (int i = 0; i < ROUNDS; i++) {
uint64_t val = rand_uint63n(cnt);
- sum += ((double)val)/(cnt-1);
+ sum += val;
test_assert(val < cnt);
if (cnt < MAX_SEE_ALL)
seen[val] = true;
}
if (cnt > 1) {
- test_assert(sum/ROUNDS > 0.45);
- test_assert(sum/ROUNDS < 0.55);
+ test_assert(sum/ROUNDS > 0.45*(cnt-1));
+ test_assert(sum/ROUNDS < 0.55*(cnt-1));
}
if (cnt < MAX_SEE_ALL) {
for (uint64_t i = 0; i < cnt; i++)
diff --git a/libmisc/tests/test_vcall.c b/libmisc/tests/test_vcall.c
deleted file mode 100644
index f36fc4b..0000000
--- a/libmisc/tests/test_vcall.c
+++ /dev/null
@@ -1,74 +0,0 @@
-/* libmisc/tests/test_vcall.c - Tests for <libmisc/vcall.h>
- *
- * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-#include <libmisc/assert.h>
-#include <libmisc/vcall.h>
-
-#include "test.h"
-
-/******************************************************************************/
-
-struct frobber_vtable;
-
-typedef struct {
- struct frobber_vtable *vtable;
-} implements_frobber;
-
-struct frobber_vtable {
- int (*frob)(implements_frobber *);
- int (*frob1)(implements_frobber *, int);
- void (*frob0)(implements_frobber *);
-};
-
-/******************************************************************************/
-
-struct myclass {
- int a;
- implements_frobber;
-};
-static_assert(offsetof(struct myclass, implements_frobber) != 0);
-
-static int myclass_frob(implements_frobber *_self) {
- struct myclass *self = VCALL_SELF(struct myclass, implements_frobber, _self);
- test_assert(self);
- test_assert((void*)self != (void*)_self);
- return self->a;
-}
-
-static int myclass_frob1(implements_frobber *_self, int arg) {
- struct myclass *self = VCALL_SELF(struct myclass, implements_frobber, _self);
- test_assert(self);
- test_assert((void*)self != (void*)_self);
- return arg;
-}
-
-static void myclass_frob0(implements_frobber *_self) {
- struct myclass *self = VCALL_SELF(struct myclass, implements_frobber, _self);
- test_assert(self);
- test_assert((void*)self != (void*)_self);
-}
-
-struct frobber_vtable myclass_vtable = {
- .frob = myclass_frob,
- .frob1 = myclass_frob1,
- .frob0 = myclass_frob0,
-};
-
-/******************************************************************************/
-
-#define MAGIC1 909837
-#define MAGIC2 657441
-
-int main() {
- struct myclass obj = {
- .implements_frobber = { .vtable = &myclass_vtable },
- .a = MAGIC1,
- };
- test_assert(VCALL(&obj, frob) == MAGIC1);
- test_assert(VCALL(&obj, frob1, MAGIC2) == MAGIC2);
- VCALL(&obj, frob0);
- return 0;
-}
diff --git a/libmisc/utf8.c b/libmisc/utf8.c
new file mode 100644
index 0000000..28357f0
--- /dev/null
+++ b/libmisc/utf8.c
@@ -0,0 +1,44 @@
+/* libmisc/utf8.c - UTF-8 routines
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/utf8.h>
+
+void utf8_decode_codepoint(const uint8_t *str, size_t len, uint32_t *ret_ch, uint8_t *ret_chlen) {
+ uint32_t ch;
+ uint8_t chlen;
+ uint32_t chmin;
+ if ((str[0] & 0b10000000) == 0b00000000) { ch = str[0] & 0b01111111; chlen = 1; chmin = 0; } /* bits=7+(0*6)= 7 */
+ else if ((str[0] & 0b11100000) == 0b11000000) { ch = str[0] & 0b00011111; chlen = 2; chmin = UINT32_C(1)<< 7; } /* bits=5+(1*6)=11 */
+ else if ((str[0] & 0b11110000) == 0b11100000) { ch = str[0] & 0b00001111; chlen = 3; chmin = UINT32_C(1)<<11; } /* bits=4+(2*6)=16 */
+ else if ((str[0] & 0b11111000) == 0b11110000) { ch = str[0] & 0b00000111; chlen = 4; chmin = UINT32_C(1)<<16; } /* bits=3+(3*6)=21 */
+ else goto invalid;
+ if (chlen > len)
+ goto invalid;
+ for (uint8_t i = 1; i < chlen; i++) {
+ if ((str[i] & 0b11000000) != 0b10000000)
+ goto invalid;
+ ch = (ch << 6) | (str[i] & 0b00111111);
+ }
+ if (ch > 0x10FFFF || ch < chmin)
+ goto invalid;
+ *ret_ch = ch;
+ *ret_chlen = chlen;
+ return;
+ invalid:
+ *ret_chlen = 0;
+}
+
+bool _utf8_is_valid(const uint8_t *str, size_t len, bool forbid_nul) {
+ for (size_t pos = 0; pos < len;) {
+ uint32_t ch;
+ uint8_t chlen;
+ utf8_decode_codepoint(&str[pos], len-pos, &ch, &chlen);
+ if (chlen == 0 || (forbid_nul && ch == 0))
+ return false;
+ pos += chlen;
+ }
+ return true;
+}
diff --git a/libmisc/wrap-cc b/libmisc/wrap-cc
new file mode 100755
index 0000000..e7a0b91
--- /dev/null
+++ b/libmisc/wrap-cc
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+# libmisc/wrap-cc - Wrapper around GCC to enhance the preprocessor
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+import os
+import subprocess
+import sys
+import typing
+
+
+def scan_tuple(
+ text: str, beg: int, on_part: typing.Callable[[str], None] | None = None
+) -> int:
+ assert text[beg] == "("
+ pos = beg + 1
+ arg_start = pos
+ parens = 1
+ instring = False
+ while parens:
+ c = text[pos]
+ if instring:
+ match c:
+ case "\\":
+ pos += 1
+ case '"':
+ instring = False
+ else:
+ match c:
+ case "(":
+ parens += 1
+ case ")":
+ parens -= 1
+ if on_part and parens == 0 and text[beg + 1 : pos].strip():
+ on_part(text[arg_start:pos])
+ case ",":
+ if on_part and parens == 1:
+ on_part(text[arg_start:pos])
+ arg_start = pos + 1
+ case '"':
+ instring = True
+ pos += 1
+ assert text[pos - 1] == ")"
+ return pos - 1
+
+
+def unquote(cstr: str) -> str:
+ assert len(cstr) >= 2 and cstr[0] == '"' and cstr[-1] == '"'
+ cstr = cstr[1:-1]
+ out = ""
+ while cstr:
+ if cstr[0] == "\\":
+ match cstr[1]:
+ case "n":
+ out += "\n"
+ cstr = cstr[2:]
+ case "\\":
+ out += "\\"
+ cstr = cstr[2:]
+ case '"':
+ out += '"'
+ cstr = cstr[2:]
+ else:
+ out += cstr[0]
+ cstr = cstr[1:]
+ return out
+
+
+def preprocess(all_args: list[str]) -> typing.NoReturn:
+ # argparse #################################################################
+ _args = all_args
+
+ def shift(n: int) -> list[str]:
+ nonlocal _args
+ ret = _args[:n]
+ _args = _args[n:]
+ return ret
+
+ arg0 = shift(1)[0]
+ common_flags: list[str] = []
+ output_flags: list[str] = []
+ positional: list[str] = []
+ while _args:
+ if len(_args[0]) > 2 and _args[0][0] == "-" and _args[0][1] in "IDU":
+ _args = [_args[0][:2], _args[0][2:], *_args[1:]]
+ match _args[0]:
+ # Mode
+ case "-E" | "-quiet":
+ common_flags += shift(1)
+ case "-lang-asm":
+ os.execvp(all_args[0], all_args)
+ # Search path
+ case "-I" | "-imultilib" | "-isystem":
+ common_flags += shift(2)
+ # Define/Undefine
+ case "-D" | "-U":
+ common_flags += shift(2)
+ # Optimization
+ case "-O0" | "-O1" | "-O2" | "-O3" | "-Os" | "-Ofast" | "-Og" | "-Oz":
+ common_flags += shift(1)
+ case "-g":
+ common_flags += shift(1)
+ # Output files
+ case "-MD" | "-MF" | "-MT" | "-dumpbase" | "-dumpbase-ext":
+ output_flags += shift(2)
+ case "-o":
+ output_flags += shift(2)
+ # Other
+ case _:
+ if _args[0].startswith("-"):
+ if _args[0].startswith("-std="):
+ common_flags += shift(1)
+ elif _args[0].startswith("-m"):
+ common_flags += shift(1)
+ elif _args[0].startswith("-f"):
+ common_flags += shift(1)
+ elif _args[0].startswith("-W"):
+ common_flags += shift(1)
+ else:
+ raise ValueError(f"unknown flag: {_args!r}")
+ else:
+ positional += shift(1)
+ if len(positional) != 1:
+ raise ValueError("expected 1 input file")
+ infile = positional[0]
+
+ # enhance ##################################################################
+
+ common_flags += ["-D", "__LIBMISC_ENHANCED_CPP__"]
+
+ text = subprocess.run(
+ [arg0, *common_flags, infile],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=sys.stderr,
+ check=True,
+ text=True,
+ ).stdout
+
+ macros: dict[str, str] = {}
+
+ marker = "__xx__LM_DEFAPPEND__xx__"
+ pos = 0
+ while (marker_beg := text.find(marker, pos)) >= 0:
+ args: list[str] = []
+
+ def add_arg(arg: str) -> None:
+ nonlocal args
+ args.append(arg)
+
+ beg_paren = marker_beg + len(marker)
+ end_paren = scan_tuple(text, beg_paren, add_arg)
+
+ before = text[:marker_beg]
+ # old = text[marker_beg : end_paren + 1]
+ after = text[end_paren + 1 :]
+
+ assert len(args) == 2
+ k = unquote(args[0].strip())
+ v = unquote(args[1].strip())
+ if k not in macros:
+ macros[k] = v
+ else:
+ macros[k] += " " + v
+
+ text = before + after
+ pos = len(before)
+
+ common_flags += ["-D", marker + "=LM_EAT"]
+ for k, v in macros.items():
+ common_flags += ["-D", k + "=" + v]
+
+ # Run, for-real ############################################################
+ os.execvp(arg0, [arg0, *common_flags, *output_flags, infile])
+
+
+def main(all_args: list[str]) -> typing.NoReturn:
+ if len(all_args) >= 2 and all_args[0].endswith("cc1") and all_args[1] == "-E":
+ preprocess(all_args)
+ else:
+ os.execvp(all_args[0], all_args)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])