diff --git a/lib/meson.build b/lib/meson.build
index 2709e2dd352e54ba3c4ea18dd2283b8aed953088..e6b642b2a3681dbc7cbc2e21efd8fa19aefe1474 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -6,6 +6,7 @@ utils_src = [
   'types/array.c',
   'types/hashtable.c',
   'types/slist.c',
+  'utils/hash.c',
   'utils/mem.c',
   'utils/utils.c',
 ]
@@ -13,6 +14,7 @@ utils_src = [
 utils_deps = [
   config_h,
   libm,
+  xxhash_dep,
 ]
 
 utils_includes = [
diff --git a/lib/utils/hash.c b/lib/utils/hash.c
new file mode 100644
index 0000000000000000000000000000000000000000..398aff45118a824859b49374308e67a911b52f3f
--- /dev/null
+++ b/lib/utils/hash.c
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: 2022 Bruce Cowan <bruce@bcowan.me.uk>
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "hash.h"
+
+#include <string.h>
+
+#include <xxhash.h>
+
+unsigned
+str_hash (const void *data)
+{
+  size_t length = strlen ((const char *) data);
+
+  XXH64_hash_t hash = XXH3_64bits (data, length);
+  return (unsigned) (hash ^ (hash >> 32));
+}
+
+bool
+str_equal (const void *a,
+           const void *b)
+{
+  return !strcmp (a, b);
+}
+
diff --git a/lib/utils/hash.h b/lib/utils/hash.h
new file mode 100644
index 0000000000000000000000000000000000000000..3de06b5d506e68b6483044d3a89097754006fcc0
--- /dev/null
+++ b/lib/utils/hash.h
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2022 Bruce Cowan <bruce@bcowan.me.uk>
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+unsigned str_hash  (const void *data);
+
+bool     str_equal (const void *a,
+                    const void *b);
diff --git a/meson.build b/meson.build
index 45e0efa8296942ebd2bebedfc6931b4455714f40..da4cd303440236d3ce22d85b2f6ba3cadbb210b4 100644
--- a/meson.build
+++ b/meson.build
@@ -4,7 +4,7 @@
 
 project('stdlib', 'c',
   default_options: ['warning_level=3', 'c_std=gnu99'],
-  meson_version: '>=0.50.0')
+  meson_version: '>=0.55.0')
 
 cc = meson.get_compiler('c')
 libm = cc.find_library('m')
@@ -12,7 +12,7 @@ libm = cc.find_library('m')
 glib_dep = dependency('glib-2.0', version: '>= 2.16', required: false)
 openmp_dep = dependency('openmp', required: false)
 thread_dep = dependency('threads', required: false)
-xxhash_dep = dependency('libxxhash', required: false)
+xxhash_dep = dependency('libxxhash')
 
 conf_data = configuration_data()
 
diff --git a/src/hashtable-test.c b/src/hashtable-test.c
index b5b5f4a0e1ddc048a0d3e0c2f9301504ea2de206..f424f332995adeaf4723ab00ff0cc179722f1e3d 100644
--- a/src/hashtable-test.c
+++ b/src/hashtable-test.c
@@ -8,8 +8,7 @@
 #include <stdlib.h>
 #include <string.h>
 
-#include <xxhash.h>
-
+#include <hash.h>
 #include <hashtable.h>
 
 static void
@@ -41,22 +40,6 @@ print_all (void *key,
   printf ("%s:%s\n", (const char *) key, (const char *) value);
 }
 
-static unsigned
-str_hash (const void *data)
-{
-  size_t length = strlen ((const char *) data);
-
-  XXH64_hash_t hash = XXH3_64bits (data, length);
-  return (unsigned) (hash ^ (hash >> 32));
-}
-
-static bool
-str_equal (const void *a,
-           const void *b)
-{
-  return !strcmp (a, b);
-}
-
 int
 main (void)
 {
diff --git a/src/meson.build b/src/meson.build
index 0891a9749b284f3dc0c6435623dfbd675f50c660..e75425c9d0a0a8f853811664bb854b0c8a08eaeb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -16,6 +16,7 @@ executable('fixed-sizeof', 'fixed-sizeof.c')
 executable('fgets', 'fgets.c')
 executable('gcd', 'gcd.c')
 executable('getchar', 'getchar.c')
+executable('hashtable-test', 'hashtable-test.c', dependencies: utils_dep)
 executable('kepler', 'kepler.c', dependencies: libm)
 executable('list-test', 'list-test.c', dependencies: utils_dep)
 executable('next', 'next.c', dependencies: libm)
@@ -44,7 +45,3 @@ endif
 if thread_dep.found()
   subdir('pthread')
 endif
-
-if xxhash_dep.found()
-  executable('hashtable-test', 'hashtable-test.c', dependencies: [utils_dep, xxhash_dep])
-endif
diff --git a/subprojects/xxhash.wrap b/subprojects/xxhash.wrap
new file mode 100644
index 0000000000000000000000000000000000000000..9019539293a1e4814a5fdd50d5bf9828d8f5fac9
--- /dev/null
+++ b/subprojects/xxhash.wrap
@@ -0,0 +1,12 @@
+[wrap-file]
+directory = xxHash-0.8.0
+source_url = https://github.com/Cyan4973/xxHash/archive/v0.8.0.tar.gz
+source_filename = xxHash-0.8.0.tar.gz
+source_hash = 7054c3ebd169c97b64a92d7b994ab63c70dd53a06974f1f630ab782c28db0f4f
+patch_filename = xxhash_0.8.0-2_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/xxhash_0.8.0-2/get_patch
+patch_hash = 793101300da5d24b7341e6b9e9e39e57e73cf1858aba6fef86adb54a7e9d58c8
+
+[provide]
+libxxhash = xxhash_dep
+