aboutsummaryrefslogtreecommitdiff
path: root/lib/libutil++
diff options
context:
space:
mode:
Diffstat (limited to 'lib/libutil++')
-rw-r--r--lib/libutil++/Makefile23
-rw-r--r--lib/libutil++/freebsd__FILE_up.341
-rw-r--r--lib/libutil++/freebsd__addrinfo_up.345
-rw-r--r--lib/libutil++/freebsd__fd_up.378
-rw-r--r--lib/libutil++/freebsd__malloc_up.350
-rw-r--r--lib/libutil++/freebsd__nvlist_up.337
-rw-r--r--lib/libutil++/freebsd__pidfile.3110
-rw-r--r--lib/libutil++/freebsd__stringf.348
-rw-r--r--lib/libutil++/libutil++.hh230
-rw-r--r--lib/libutil++/stringf.cc57
-rw-r--r--lib/libutil++/tests/Makefile12
-rw-r--r--lib/libutil++/tests/pidfile_test.cc44
-rw-r--r--lib/libutil++/tests/stringf_test.cc52
-rw-r--r--lib/libutil++/tests/up_test.cc33
14 files changed, 860 insertions, 0 deletions
diff --git a/lib/libutil++/Makefile b/lib/libutil++/Makefile
new file mode 100644
index 000000000000..2e7a614df800
--- /dev/null
+++ b/lib/libutil++/Makefile
@@ -0,0 +1,23 @@
+LIB_CXX= util++
+INTERNALLIB= true
+SHLIB_MAJOR= 1
+SRCS= stringf.cc
+
+MAN+= freebsd::FILE_up.3 \
+ freebsd::addrinfo_up.3 \
+ freebsd::fd_up.3 \
+ freebsd::malloc_up.3 \
+ freebsd::nvlist_up.3 \
+ freebsd::pidfile.3 \
+ freebsd::stringf.3
+
+.for page in ${MAN}
+MANSRC.${page}= ${page:S/:/_/g}
+.endfor
+
+.include <src.opts.mk>
+
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
+.include <bsd.lib.mk>
diff --git a/lib/libutil++/freebsd__FILE_up.3 b/lib/libutil++/freebsd__FILE_up.3
new file mode 100644
index 000000000000..ea63b1233b43
--- /dev/null
+++ b/lib/libutil++/freebsd__FILE_up.3
@@ -0,0 +1,41 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::FILE_UP 3
+.Os
+.Sh NAME
+.Nm freebsd::FILE_up
+.Nd std::unique_ptr specialization for stdio FILE objects
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Ft using FILE_up = std::unique_ptr<FILE, fclose_deleter>;
+.Sh DESCRIPTION
+This class is a specialization of
+.Vt std::unique_ptr
+for stdio
+.Vt FILE
+objects.
+When a
+.Vt FILE
+object managed by an instance of this class is disposed,
+.Xr fclose 3
+is invoked to dispose of the
+.Vt FILE
+object.
+.Sh EXAMPLES
+.Bd -literal -offset indent
+freebsd::FILE_up fp(fopen("foo.txt", "w"));
+if (!fp)
+ err(1, "fopen");
+fprintf(fp.get(), "hello\n");
+// `fp' is implicitly closed on destruction
+.Ed
+.Sh SEE ALSO
+.Xr fclose 3 ,
+.Xr fopen 3
diff --git a/lib/libutil++/freebsd__addrinfo_up.3 b/lib/libutil++/freebsd__addrinfo_up.3
new file mode 100644
index 000000000000..4845a76bfb61
--- /dev/null
+++ b/lib/libutil++/freebsd__addrinfo_up.3
@@ -0,0 +1,45 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::ADDRINFO_UP 3
+.Os
+.Sh NAME
+.Nm freebsd::addrinfo_up
+.Nd std::unique_ptr specialization for lists of socket addresses
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Ft using addrinfo_up = std::unique_ptr<addrinfo, freeaddrinfo_deleter>;
+.Sh DESCRIPTION
+This class is a specialization of
+.Vt std::unique_ptr
+for socket addresses returned by
+.Xr getaddrinfo 3 .
+When a list of socket addresses managed by an instance of this class is
+disposed,
+.Xr freeaddrinfo 3
+is invoked to dispose of the list.
+.Sh EXAMPLES
+.Bd -literal -offset indent
+freebsd::addrinfo_up
+resolve_address(const char *address, const char *port)
+{
+ struct addrinfo hints, *ai;
+ int error;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ error = getaddrinfo(address, port, &hints, &ai);
+ if (error != 0)
+ return {};
+ return freebsd::addrinfo_up(ai);
+}
+.Ed
+.Sh SEE ALSO
+.Xr getaddrinfo 3
diff --git a/lib/libutil++/freebsd__fd_up.3 b/lib/libutil++/freebsd__fd_up.3
new file mode 100644
index 000000000000..2ef2241a5c40
--- /dev/null
+++ b/lib/libutil++/freebsd__fd_up.3
@@ -0,0 +1,78 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::STRINGF 3
+.Os
+.Sh NAME
+.Nm freebsd::fd_up
+.Nd own a file descriptor
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Pp
+.Vt class freebsd::fd_up
+{
+.Bd -ragged -offset indent
+.Fn fd_up
+.Fn fd_up "int fd"
+.Fn fd_up "fd_up &&other"
+.Fn ~fd_up
+.Ft int
+.Fn get
+.Ft int
+.Fn release
+.Ft void
+.Fn reset "int newfd = -1"
+.Ft "fd_up &"
+.Fn operator= "fd_up &&other"
+.Ft "fd_up &"
+.Fn operator= "int fd"
+.Fn "explicit operator bool"
+.Fn "operator int"
+.Ed
+};
+.Sh DESCRIPTION
+Each instance of this class owns a file descriptor.
+This class is patterned on std::unique_ptr,
+but instead of owning a pointer to an object,
+this class owns a file descriptor.
+The currently-owned file descriptor is disposed by invoking
+.Xr close 2
+when an instance of this class is destroyed.
+The currently-owned file descriptor is also disposed if it is replaced by the
+.Fn reset
+method or assignment operators.
+.Pp
+The
+.Fn get
+method returns the current file descriptor value while retaining ownership.
+.Pp
+The
+.Fn release
+method relinquishes ownership of the current file descriptor and returns the
+value of the previously-owned file descriptor.
+.Pp
+The explicit
+.Vt bool
+conversion operator permits testing the validity of an object.
+The operator returns true if the instance owns a valid file descriptor.
+.Pp
+The implicit
+.Vt int
+conversion operator permits passing an instance of this class directly as
+an argument to existing functions which expect a file descriptor.
+.Sh EXAMPLES
+.Bd -literal -offset indent
+freebsd::fd_up fd(open("/dev/null", O_RDWR));
+if (!fd)
+ err(1, "open");
+write(fd, "test", 4);
+// `fd' is implicitly closed on destruction
+.Ed
+.Sh SEE ALSO
+.Xr close 2
diff --git a/lib/libutil++/freebsd__malloc_up.3 b/lib/libutil++/freebsd__malloc_up.3
new file mode 100644
index 000000000000..b18e7854213a
--- /dev/null
+++ b/lib/libutil++/freebsd__malloc_up.3
@@ -0,0 +1,50 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::MALLOC_UP 3
+.Os
+.Sh NAME
+.Nm freebsd::malloc_up
+.Nd std::unique_ptr specialization for objects allocated via malloc
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Ft using malloc_up = std::unique_ptr<T, free_deleter<T>>;
+.Sh DESCRIPTION
+This class is a specialization of
+.Vt std::unique_ptr
+which invokes
+.Xr free 3
+instead of
+.Fn delete
+when an object is disposed.
+While explicit calls to
+.Xr malloc 3
+should be avoided in C++ code,
+this class can be useful to manage an object allocated by an existing API
+which uses
+.Xr malloc 3
+internally such as
+.Xr scandir 3 .
+Note that the type of the underlying object must be used as the first
+template argument similar to std::unique_ptr.
+.Sh EXAMPLES
+This example uses
+.Xr strdup 3
+for simplicity,
+but new C++ code should generally not use
+.Xr strdup 3 :
+.Bd -literal -offset indent
+freebsd::malloc_up<char> my_string(strdup("foo"));
+// `mystring' is implicitly freed on destruction
+.Ed
+.Sh SEE ALSO
+.Xr free 3 ,
+.Xr malloc 3 ,
+.Xr scandir 3 ,
+.Xr strdup 3
diff --git a/lib/libutil++/freebsd__nvlist_up.3 b/lib/libutil++/freebsd__nvlist_up.3
new file mode 100644
index 000000000000..43f76cf3ead3
--- /dev/null
+++ b/lib/libutil++/freebsd__nvlist_up.3
@@ -0,0 +1,37 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::NVLIST_UP 3
+.Os
+.Sh NAME
+.Nm freebsd::nvlist_up
+.Nd std::unique_ptr specialization for name/value pairs
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Ft using nvlist_up = std::unique_ptr<nvlist, nvlist_deleter>;
+.Sh DESCRIPTION
+This class is a specialization of
+.Vt std::unique_ptr
+for
+.Vt nvlist_t
+objects.
+When an
+.Vt nvlist_t
+object managed by an instance of this class is disposed,
+.Xr nvlist_destroy 3
+is invoked to dispose of the object.
+.Sh EXAMPLES
+.Bd -literal -offset indent
+freebsd::nvlist_up nvl(nvlist_create(0));
+nvlist_add_number(nvl.get(), "answer", 42);
+nvlist_add_bool(nvl.get(), "valid", true);
+// `nvl' is implicitly destroyed
+.Ed
+.Sh SEE ALSO
+.Xr nvlist_destroy 3
diff --git a/lib/libutil++/freebsd__pidfile.3 b/lib/libutil++/freebsd__pidfile.3
new file mode 100644
index 000000000000..fb67253f5c02
--- /dev/null
+++ b/lib/libutil++/freebsd__pidfile.3
@@ -0,0 +1,110 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::STRINGF 3
+.Os
+.Sh NAME
+.Nm freebsd::pidfile
+.Nd own a PID file handle
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Pp
+.Vt class freebsd::pidfile
+{
+.Bd -ragged -offset indent
+.Fn pidfile
+.Fn pidfile "struct pidfh *pfh"
+.Fn pidfile "pidfile &&other"
+.Fn ~pidfile
+.Ft struct pidfh *
+.Fn release
+.Ft void
+.Fn reset "struct pidfh *newpfh = nullptr"
+.Ft int
+.Fn write
+.Ft int
+.Fn close
+.Ft int
+.Fn fileno
+.Ft "pidfile &"
+.Fn operator= "pidfile &&other"
+.Ft "pidfile &"
+.Fn operator= "struct pidfh *pfh"
+.Fn "explicit operator bool"
+.Ed
+};
+.Sh DESCRIPTION
+Each instance of this class owns a PID file handle created by
+.Xr pidfile_open 3 .
+This class is patterned on std::unique_ptr;
+however,
+rather than exporting the raw pointer via a
+.Fn get
+method,
+this class provides wrapper methods for each of the other
+.Xr pidfile 3
+functions.
+The currently-owned PID file is removed by invoking
+.Xr pidfile_remove 3
+when an instance of this class is destroyed.
+The currently-owned PID file is also removed if it is replaced by the
+.Fn reset
+method or assignment operators.
+.Pp
+The
+.Fn release
+method relinquishes ownership of the current PID file handle and returns the
+value of the previously-owned PID file handle.
+.Pp
+The
+.Fn write
+method writes out the PID of the current process to the PID file via
+.Xr pidfile_write 3 .
+.Pp
+The
+.Fn close
+method closes the current PID file without removing it via
+.Xr pidfile_close 3 .
+If the close succeeds, the PID file handle is no longer valid.
+.Pp
+The
+.Fn fileno
+method returns the underlying file descriptor for the current PID file via
+.Xr pidfile_fileno 3 .
+.Pp
+The explicit
+.Vt bool
+conversion operator permits testing the validity of an object.
+The operator returns true if the instance owns a valid PID file handle.
+.Sh EXAMPLES
+.Bd -literal -offset indent
+int
+main()
+{
+ freebsd::pidfile pf(pidfile_open("/var/run/daemon.pid",
+ 0600, NULL));
+ if (!pf)
+ err(1, "pidfile_open");
+
+ if (daemon(0, 0) == -1) {
+ warn("daemon");
+ return 1;
+ }
+
+ pf->write();
+
+ for (;;) {
+ /* Do Work */
+ }
+
+ return 0;
+}
+.Ed
+.Sh SEE ALSO
+.Xr pidfile 3
diff --git a/lib/libutil++/freebsd__stringf.3 b/lib/libutil++/freebsd__stringf.3
new file mode 100644
index 000000000000..341fedef4343
--- /dev/null
+++ b/lib/libutil++/freebsd__stringf.3
@@ -0,0 +1,48 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2025 Chelsio Communications, Inc.
+.\" Written by: John Baldwin <jhb@FreeBSD.org>
+.\"
+.Dd July 31, 2025
+.Dt FREEBSD::STRINGF 3
+.Os
+.Sh NAME
+.Nm freebsd::stringf
+.Nd build a std::string using stdio formatting
+.Sh LIBRARY
+.Lb libutil++
+.Sh SYNOPSIS
+.In libutil++.hh
+.Ft std::string
+.Fn freebsd::stringf "const char *fmt" "..."
+.Ft std::string
+.Fn freebsd::stringf "const char *fmt" "va_list ap"
+.Sh DESCRIPTION
+These functions construct a
+.Vt std::string
+object containing a formatted string.
+The output of the string is dictated by the
+.Fa fmt
+argument and additional arguments using the same conventions documented in
+.Xr printf 3 .
+The first form provides functionality similar to
+.Xr asprintf 3
+and the second form is similar to
+.Xr vasprintf 3 .
+.Sh RETURN VALUES
+These functions return a std::string object.
+.Sh EXCEPTIONS
+These functions may throw one of the following exceptions:
+.Bl -tag -width Er
+.It Bq Er std::bad_alloc
+Failed to allocate memory.
+.It Bq Er std::length_error
+The result would exceeed the maximum possible string size.
+.El
+.Sh EXAMPLES
+.Bd -literal -offset indent
+std::string s = freebsd::stringf("hello %s", "world");
+.Ed
+.Sh SEE ALSO
+.Xr asprintf 3
diff --git a/lib/libutil++/libutil++.hh b/lib/libutil++/libutil++.hh
new file mode 100644
index 000000000000..60e6b3fc5fde
--- /dev/null
+++ b/lib/libutil++/libutil++.hh
@@ -0,0 +1,230 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#ifndef __LIBUTILPP_HH__
+#define __LIBUTILPP_HH__
+
+#include <sys/nv.h>
+#include <libutil.h>
+#include <netdb.h>
+#include <unistd.h>
+
+#include <cstdarg>
+#include <cstdio>
+#include <cstdlib>
+#include <memory>
+
+namespace freebsd {
+ /*
+ * FILE_up is a std::unique_ptr<> for FILE objects which uses
+ * fclose() to destroy the wrapped pointer.
+ */
+ struct fclose_deleter {
+ void operator() (std::FILE *fp) const
+ {
+ std::fclose(fp);
+ }
+ };
+
+ using FILE_up = std::unique_ptr<std::FILE, fclose_deleter>;
+
+ /*
+ * addrinfo_up is a std::unique_ptr<> which uses
+ * freeaddrinfo() to destroy the wrapped pointer. It is
+ * intended to wrap arrays allocated by getaddrinfo().
+ */
+ struct freeaddrinfo_deleter {
+ void operator() (struct addrinfo *ai) const
+ {
+ freeaddrinfo(ai);
+ }
+ };
+
+ using addrinfo_up = std::unique_ptr<addrinfo, freeaddrinfo_deleter>;
+
+ /*
+ * This class is intended to function similar to unique_ptr<>,
+ * but it contains a file descriptor rather than a pointer to
+ * an object. On destruction the descriptor is closed via
+ * close(2).
+ *
+ * Similar to unique_ptr<>, release() returns ownership of the
+ * file descriptor to the caller. reset() closes the current
+ * file descriptor and takes ownership of a new one. A move
+ * constructor permits ownership to be transferred via
+ * std::move(). An integer file descriptor can be assigned
+ * directly which is equivalent to calling reset().
+ *
+ * An explicit bool conversion operator permits testing this
+ * class in logical expressions. It returns true if it
+ * contains a valid descriptor.
+ *
+ * An implicit int conversion operator returns the underlying
+ * file descriptor allowing objects of this type to be passed
+ * directly to APIs such as connect(), listen(), etc.
+ */
+ class fd_up {
+ public:
+ fd_up() : fd(-1) {}
+ fd_up(int _fd) : fd(_fd) {}
+ fd_up(fd_up &&other) : fd(other.release()) {}
+ fd_up(fd_up const &) = delete;
+
+ ~fd_up() { reset(); }
+
+ int get() const { return (fd); }
+
+ int release()
+ {
+ int oldfd = fd;
+
+ fd = -1;
+ return (oldfd);
+ }
+
+ void reset(int newfd = -1)
+ {
+ if (fd >= 0)
+ close(fd);
+ fd = newfd;
+ }
+
+ fd_up &operator=(fd_up &&other) noexcept
+ {
+ if (this == &other)
+ return *this;
+
+ reset(other.release());
+ return *this;
+ }
+
+ fd_up &operator=(fd_up const &) = delete;
+
+ fd_up &operator=(int newfd)
+ {
+ reset(newfd);
+ return *this;
+ }
+
+ explicit operator bool() const { return fd >= 0; }
+ operator int() const { return fd; }
+ private:
+ int fd;
+ };
+
+ /*
+ * malloc_up<T> is a std::unique_ptr<> which uses free() to
+ * destroy the wrapped pointer. This can be used to wrap
+ * pointers allocated implicitly by malloc() such as those
+ * returned by strdup().
+ */
+ template <class T>
+ struct free_deleter {
+ void operator() (T *p) const
+ {
+ std::free(p);
+ }
+ };
+
+ template <class T>
+ using malloc_up = std::unique_ptr<T, free_deleter<T>>;
+
+ /*
+ * nvlist_up is a std::unique_ptr<> for nvlist_t objects which
+ * uses nvlist_destroy() to destroy the wrapped pointer.
+ */
+ struct nvlist_deleter {
+ void operator() (nvlist_t *nvl) const
+ {
+ nvlist_destroy(nvl);
+ }
+ };
+
+ using nvlist_up = std::unique_ptr<nvlist_t, nvlist_deleter>;
+
+ /*
+ * A wrapper class for the pidfile_* API. The destructor
+ * calls pidfile_remove() when an object is destroyed. This
+ * class is similar to std::unique_ptr<> in that it retains
+ * exclusive ownership of the pidfh object.
+ *
+ * In addition to release() and reset methods(), write(),
+ * close(), and fileno() methods are provided as wrappers for
+ * pidfile_*.
+ */
+ class pidfile {
+ public:
+ pidfile() = default;
+ pidfile(struct pidfh *_pfh) : pfh(_pfh) {}
+ pidfile(pidfile &&other) : pfh(other.release()) {}
+ pidfile(pidfile const &) = delete;
+
+ ~pidfile() { reset(); }
+
+ struct pidfh *release()
+ {
+ struct pidfh *oldpfh = pfh;
+
+ pfh = nullptr;
+ return (oldpfh);
+ }
+
+ void reset(struct pidfh *newpfh = nullptr)
+ {
+ if (pfh != nullptr)
+ pidfile_remove(pfh);
+ pfh = newpfh;
+ }
+
+ int write()
+ {
+ return (pidfile_write(pfh));
+ }
+
+ int close()
+ {
+ int rv = pidfile_close(pfh);
+ if (rv == 0)
+ pfh = nullptr;
+ return (rv);
+ }
+
+ int fileno()
+ {
+ return (pidfile_fileno(pfh));
+ }
+
+ pidfile &operator=(pidfile &&other) noexcept
+ {
+ if (this == &other)
+ return *this;
+ reset(other.release());
+ return *this;
+ }
+
+ pidfile &operator=(pidfile const &) = delete;
+
+ pidfile &operator=(struct pidfh *newpfh)
+ {
+ reset(newpfh);
+ return *this;
+ }
+
+ explicit operator bool() const { return pfh != nullptr; }
+ private:
+ struct pidfh *pfh = nullptr;
+ };
+
+ /*
+ * Returns a std::string containing the same output as
+ * sprintf(). Throws std::bad_alloc if an error occurs.
+ */
+ std::string stringf(const char *fmt, ...) __printflike(1, 2);
+ std::string stringf(const char *fmt, std::va_list ap);
+}
+
+#endif /* !__LIBUTILPP_HH__ */
diff --git a/lib/libutil++/stringf.cc b/lib/libutil++/stringf.cc
new file mode 100644
index 000000000000..8c24167d70ac
--- /dev/null
+++ b/lib/libutil++/stringf.cc
@@ -0,0 +1,57 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <cstdarg>
+#include <cstdio>
+#include <string>
+
+#include "libutil++.hh"
+
+static int
+stringf_write(void *cookie, const char *buf, int len)
+{
+ std::string *str = reinterpret_cast<std::string *>(cookie);
+ try {
+ str->append(buf, len);
+ } catch (std::bad_alloc) {
+ errno = ENOMEM;
+ return (-1);
+ } catch (std::length_error) {
+ errno = EFBIG;
+ return (-1);
+ }
+ return (len);
+}
+
+std::string
+freebsd::stringf(const char *fmt, va_list ap)
+{
+ std::string str;
+ freebsd::FILE_up fp(fwopen(reinterpret_cast<void *>(&str),
+ stringf_write));
+
+ vfprintf(fp.get(), fmt, ap);
+
+ if (ferror(fp.get()))
+ throw std::bad_alloc();
+ fp.reset(nullptr);
+
+ return str;
+}
+
+std::string
+freebsd::stringf(const char *fmt, ...)
+{
+ std::va_list ap;
+ std::string str;
+
+ va_start(ap, fmt);
+ str = freebsd::stringf(fmt, ap);
+ va_end(ap);
+
+ return str;
+}
diff --git a/lib/libutil++/tests/Makefile b/lib/libutil++/tests/Makefile
new file mode 100644
index 000000000000..e7720d122f36
--- /dev/null
+++ b/lib/libutil++/tests/Makefile
@@ -0,0 +1,12 @@
+PACKAGE= tests
+
+ATF_TESTS_CXX+= pidfile_test
+ATF_TESTS_CXX+= stringf_test
+ATF_TESTS_CXX+= up_test
+
+CFLAGS+= -I${SRCTOP}/lib/libutil++
+LIBADD+= util++
+
+LIBADD.pidfile_test+= util
+
+.include <bsd.test.mk>
diff --git a/lib/libutil++/tests/pidfile_test.cc b/lib/libutil++/tests/pidfile_test.cc
new file mode 100644
index 000000000000..067f10e8fab8
--- /dev/null
+++ b/lib/libutil++/tests/pidfile_test.cc
@@ -0,0 +1,44 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <atf-c++.hpp>
+#include <sys/stat.h>
+#include <libutil.h>
+
+#include <libutil++.hh>
+
+ATF_TEST_CASE_WITHOUT_HEAD(basic);
+ATF_TEST_CASE_BODY(basic)
+{
+ pid_t other;
+ struct pidfh *pfh = pidfile_open("test_pidfile", 0600, &other);
+ ATF_REQUIRE(pfh != nullptr);
+ ATF_REQUIRE(pidfile_fileno(pfh) >= 0);
+
+ struct stat sb;
+ ATF_REQUIRE(fstat(pidfile_fileno(pfh), &sb) == 0);
+ ATF_REQUIRE_EQ(0, sb.st_size);
+
+ freebsd::pidfile pf(pfh);
+ ATF_REQUIRE_EQ(pidfile_fileno(pfh), pf.fileno());
+
+ ATF_REQUIRE(pf.write() == 0);
+
+ ATF_REQUIRE(fstat(pf.fileno(), &sb) == 0);
+ ATF_REQUIRE(sb.st_size > 0);
+
+ ATF_REQUIRE(pf.close() == 0);
+ ATF_REQUIRE(pf.fileno() == -1);
+ ATF_REQUIRE_EQ(EDOOFUS, errno);
+
+ ATF_REQUIRE(unlink("test_pidfile") == 0);
+}
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, basic);
+}
diff --git a/lib/libutil++/tests/stringf_test.cc b/lib/libutil++/tests/stringf_test.cc
new file mode 100644
index 000000000000..5b8ef4ad54a9
--- /dev/null
+++ b/lib/libutil++/tests/stringf_test.cc
@@ -0,0 +1,52 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <atf-c++.hpp>
+#include <stdarg.h>
+#include <stdio.h>
+
+#include <libutil++.hh>
+
+ATF_TEST_CASE_WITHOUT_HEAD(basic);
+ATF_TEST_CASE_BODY(basic)
+{
+ ATF_REQUIRE_EQ("foo", freebsd::stringf("foo"));
+ ATF_REQUIRE_EQ("bar", freebsd::stringf("%s", "bar"));
+ ATF_REQUIRE_EQ("42", freebsd::stringf("%u", 42));
+ ATF_REQUIRE_EQ("0xdeadbeef", freebsd::stringf("%#x", 0xdeadbeef));
+ ATF_REQUIRE_EQ("", freebsd::stringf(""));
+ ATF_REQUIRE_EQ("this is a test", freebsd::stringf("this %s test",
+ "is a"));
+}
+
+static std::string
+stringv(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ std::string str = freebsd::stringf(fmt, ap);
+ va_end(ap);
+ return (str);
+}
+
+ATF_TEST_CASE_WITHOUT_HEAD(va_list);
+ATF_TEST_CASE_BODY(va_list)
+{
+ ATF_REQUIRE_EQ("foo", stringv("foo"));
+ ATF_REQUIRE_EQ("bar", stringv("%s", "bar"));
+ ATF_REQUIRE_EQ("42", stringv("%u", 42));
+ ATF_REQUIRE_EQ("0xdeadbeef", stringv("%#x", 0xdeadbeef));
+ ATF_REQUIRE_EQ("", stringv(""));
+ ATF_REQUIRE_EQ("this is a test", stringv("this %s test", "is a"));
+}
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, basic);
+ ATF_ADD_TEST_CASE(tcs, va_list);
+}
diff --git a/lib/libutil++/tests/up_test.cc b/lib/libutil++/tests/up_test.cc
new file mode 100644
index 000000000000..3f344054c334
--- /dev/null
+++ b/lib/libutil++/tests/up_test.cc
@@ -0,0 +1,33 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <atf-c++.hpp>
+#include <libutil.h>
+
+#include <libutil++.hh>
+
+ATF_TEST_CASE_WITHOUT_HEAD(FILE_up);
+ATF_TEST_CASE_BODY(FILE_up)
+{
+ FILE *fp = fopen("/dev/null", "r");
+ ATF_REQUIRE(fp != NULL);
+ ATF_REQUIRE(fileno(fp) != -1);
+
+ freebsd::FILE_up f(fp);
+ ATF_REQUIRE_EQ(fileno(fp), fileno(f.get()));
+
+ f.reset();
+ ATF_REQUIRE_EQ(f.get(), nullptr);
+
+ ATF_REQUIRE_EQ(-1, fileno(fp));
+ ATF_REQUIRE_EQ(EBADF, errno);
+}
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, FILE_up);
+}