diff --git a/include/neo/hashtab.h b/include/neo/hashtab.h new file mode 100644 index 0000000..4aa6d04 --- /dev/null +++ b/include/neo/hashtab.h @@ -0,0 +1,131 @@ +/** See the end of this file for copyright and license terms. */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "neo/_error.h" +#include "neo/_stddef.h" +#include "neo/_types.h" +#include "neo/list.h" + +struct _neo_hashtab_entry { + listnode_t link; + nbuf_t *key; + void *val; +}; + +struct _neo_hashtab { + NLEN_FIELD(_len); + NREF_FIELD; + u32 (*_hashfn)(const nbuf_t *key, u32 limit); + u32 _buckets_len; + list_t _buckets[0]; /* -> _neo_hashtab_entry::link */ +}; +typedef struct _neo_hashtab hashtab_t; + +/** + * Create a new hash table. + * + * The hashing function used is implementation defined; use + * `hashtab_create_custom` to specify your own. + * If allocation fails or `buckets` is 0, an error is yeeted. + * + * @param buckets: Number of hash buckets + * @param err: Error pointer + * @returns The initialized hash table, unless an error occurred + */ +hashtab_t *hashtab_create(u32 buckets, error *err); + +/** + * Create a new hash table with custom hashing algorithm. + * + * If allocation fails, `buckets` is 0, or `hashfn` is nil, an error is yeeted. + * The custom hash function must return a value less than the `limit` parameter + * passed to it. The `limit` parameter is the number of buckets minus one. + * + * @param buckets: Number of hash buckets + * @param hashfn: Custom hash function to use + * @param err: Error pointer + * @returns The initialized hash table, unless an error occurred + */ +hashtab_t *hashtab_create_custom(u32 buckets, + u32 (*hashfn)(const nbuf_t *key, u32 limit), + error *err); + +/** + * Get an entry in a hash table. + * + * If the key does not exist, *no* error is yeeted and the return value is `nil`. + * If the key is `nil` or the hash function returned a value greater than the + * maxval parameter passed to it, an error is yeeted. + * + * @param table: Hash table to get the entry from + * @param key: Key to get the value of + * @param err: Error pointer + * @returns The entry or `nil` if it does not exist, unless an error occurred + */ +void *hashtab_get(hashtab_t *table, const nbuf_t *key, error *err); + +/** + * Put an entry into a hash table. + * + * The reference counter in `key` is incremented. + * If `key` is `nil`, `key_size` is 0, the key already exists in the table, + * or the hash function returned a value greater than or equal to the `limit` + * parameter passed to it, an error is yeeted. + * + * @param table: Hash table to insert the value at + * @param key: Key to insert the value under + * @param val: Value to insert + * @param err: Error pointer + */ +void hashtab_put(hashtab_t *table, nbuf_t *key, void *val, error *err); + +/** + * Delete an entry from a hash table. + * + * If `table` or `key` is `nil`, or the key was not found within the table, + * an error is yeeted. + * + * @param table: Table to delete an entry from + * @param key: Key of the item to delete + * @param err: Error pointer + * @returns The removed item, unless an error occurred + */ +void *hashtab_del(hashtab_t *table, nbuf_t *key, error *err); + +/** + * Iterate over every entry in a hash table. + * + * If `table` or `callback` is `nil`, an error is yeeted. + * + * @param table: Table to iterate over + * @param callback: Callback function that is invoked for every entry; + * the iteration stops if the return value is nonzero + * @param extra: Optional pointer that is passed as an extra argument to the + * callback function + * @returns The last return value of the callback, unless an error occurred + */ +int hashtab_foreach(hashtab_t *table, + int (*callback)(hashtab_t *table, nbuf_t *key, void *val, void *extra), + void *extra, error *err); + +#ifdef __cplusplus +}; /* extern "C" */ +#endif + +/* + * This file is part of libneo. + * Copyright (c) 2021 Fefie . + * + * libneo is non-violent software: you may only use, redistribute, + * and/or modify it under the terms of the CNPLv6+ as found in + * the LICENSE file in the source code root directory or at + * . + * + * libneo comes with ABSOLUTELY NO WARRANTY, to the extent + * permitted by applicable law. See the CNPLv6+ for details. + */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 991dd2b..cc11af7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,7 @@ target_include_directories(neo PRIVATE target_sources(neo PRIVATE ./error.c + ./hashtab.c ./list.c ./nalloc.c ./nbuf.c diff --git a/src/hashtab.c b/src/hashtab.c new file mode 100644 index 0000000..090be94 --- /dev/null +++ b/src/hashtab.c @@ -0,0 +1,225 @@ +/** See the end of this file for copyright and license terms. */ + +#include + +#include "neo/_error.h" +#include "neo/_nalloc.h" +#include "neo/_nbuf.h" +#include "neo/_nref.h" +#include "neo/_types.h" +#include "neo/hashtab.h" +#include "neo/list.h" + +static void hashtab_destroy(hashtab_t *table) +{ + for (u32 i = 0; i < table->_buckets_len; i++) { + struct _neo_hashtab_entry *cursor; + list_foreach(cursor, &table->_buckets[i], link) { + nput(cursor->key); + nfree(cursor); + } + } + nfree(table); +} + +hashtab_t *hashtab_create_custom(u32 buckets, + u32 (*hashfn)(const nbuf_t *key, u32 limit), + error *err) +{ + if (buckets == 0) { + yeet(err, ERANGE, "Number of buckets is 0"); + return nil; + } + if (hashfn == nil) { + yeet(err, EFAULT, "Hash function is nil"); + return nil; + } + + struct _neo_hashtab *table; + usize buckets_size = sizeof(table->_buckets[0]) * buckets; + table = nalloc(sizeof(*table) + buckets_size, err); + catch(err) { + return nil; + } + + table->_len = 0; + table->_hashfn = hashfn; + table->_buckets_len = buckets; + for (usize i = 0; i < buckets; i++) + list_init(&table->_buckets[i]); + nref_init(table, hashtab_destroy); + + neat(err); + return table; +} + +/* djb2 */ +static u32 hashtab_default_hashfn(const nbuf_t *key, u32 limit) +{ + u32 hash = 0; + const u8 *cursor; + + nbuf_foreach(cursor, key) { + hash = (hash * 33) ^ *cursor; + } + + /* TODO: we can probably do better than this */ + return hash % limit; +} + +hashtab_t *hashtab_create(u32 buckets, error *err) +{ + return hashtab_create_custom(buckets, hashtab_default_hashfn, err); +} + +static u32 hashtab_compute_hash(hashtab_t *table, const nbuf_t *key, error *err) +{ + if (table == nil) { + yeet(err, EFAULT, "Hash table is nil"); + return 0; + } + if (key == nil) { + yeet(err, EFAULT, "Key is nil"); + return 0; + } + + u32 hash = table->_hashfn(key, table->_buckets_len - 1); + if (hash >= table->_buckets_len) { + yeet(err, ERANGE, "Hash function returned value outside range"); + return 0; + } + + neat(err); + return hash; +} + +static struct _neo_hashtab_entry *hashtab_find_entry(hashtab_t *table, + const nbuf_t *key, + error *err) +{ + u32 hash = hashtab_compute_hash(table, key, err); + catch(err) { + return nil; + } + + list_t *bucket = &table->_buckets[hash]; + struct _neo_hashtab_entry *cursor; + bool found = false; + list_foreach(cursor, bucket, link) { + if (nbuf_eq(cursor->key, key, nil)) { + found = true; + break; + } + } + if (!found) + cursor = nil; + + neat(err); + return cursor; +} + +void *hashtab_get(hashtab_t *table, const nbuf_t *key, error *err) +{ + struct _neo_hashtab_entry *entry = hashtab_find_entry(table, key, err); + catch(err) { + return nil; + } + + if (entry == nil) + return nil; + else + return entry->val; +} + +void hashtab_put(hashtab_t *table, nbuf_t *key, void *val, error *err) +{ + /* TODO: avoid double hash computing */ + error get_err; + struct _neo_hashtab_entry *existing_entry = hashtab_find_entry(table, key, err); + catch(err) { + return; + } + if (existing_entry != nil) { + yeet(err, EEXIST, "Key already present"); + return; + } + + u32 hash = table->_hashfn(key, table->_buckets_len - 1); + if (hash >= table->_buckets_len) { + yeet(err, ERANGE, "Hash function returned value out of range"); + return; + } + + struct _neo_hashtab_entry *entry = nalloc(sizeof(*entry), err); + catch(err) { + return; + } + nget(key); + entry->key = key; + entry->val = val; + + list_t *bucket = &table->_buckets[hash]; + list_add(bucket, &entry->link); + table->_len++; + neat(err); +} + +void *hashtab_del(hashtab_t *table, nbuf_t *key, error *err) +{ + struct _neo_hashtab_entry *entry = hashtab_find_entry(table, key, err); + catch(err) { + return nil; + } + + if (entry != nil) { + list_del(&entry->link); + table->_len--; + nput(entry->key); + return entry->val; + } else { + return nil; + } +} + +int hashtab_foreach(hashtab_t *table, + int (*callback)(hashtab_t *table, nbuf_t *key, void *val, void *extra), + void *extra, error *err) +{ + if (table == nil) { + yeet(err, EFAULT, "Hash table is nil"); + return 0; + } + if (callback == nil) { + yeet(err, EFAULT, "Callback is nil"); + return 0; + } + + int ret = 0; + for (u32 i = 0; i < table->_buckets_len; i++) { + struct _neo_hashtab_entry *cursor; + list_foreach(cursor, &table->_buckets[i], link) { + ret = callback(table, cursor->key, cursor->val, extra); + if (ret != 0) + break; + } + + if (ret != 0) + break; + } + + neat(err); + return ret; +} + +/* + * This file is part of libneo. + * Copyright (c) 2021 Fefie . + * + * libneo is non-violent software: you may only use, redistribute, + * and/or modify it under the terms of the CNPLv6+ as found in + * the LICENSE file in the source code root directory or at + * . + * + * libneo comes with ABSOLUTELY NO WARRANTY, to the extent + * permitted by applicable law. See the CNPLv6+ for details. + */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ab6da87..966997b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ include(nbuf/nbuf.cmake) include(string/string.cmake) target_sources(neo_test PRIVATE + hashtab.cpp list.cpp nref.cpp ) diff --git a/test/hashtab.cpp b/test/hashtab.cpp new file mode 100644 index 0000000..2c09fa6 --- /dev/null +++ b/test/hashtab.cpp @@ -0,0 +1,98 @@ +/** See the end of this file for copyright and license terms. */ + +#include +#include +#include + +#include +#include + +extern "C" struct test_item { + unsigned int number; +}; + +SCENARIO( "hashtab: items can be inserted and removed", "[src/hashtab.c]" ) +{ + GIVEN( "an empty hash table" ) + { + error err; + hashtab_t *table = hashtab_create(32, &err); + + REQUIRE( errnum(&err) == 0 ); + REQUIRE( table != nil ); + + WHEN( "a new item is inserted" ) + { + nbuf_t *key1 = nbuf_from_str("key1", nil); + struct test_item val1 = { + .number = 1, + }; + hashtab_put(table, key1, &val1, &err); + REQUIRE( errnum(&err) == 0 ); + + THEN( "the item can be retrieved again" ) + { + nbuf_t *key1_clone = nbuf_clone(key1, nil); + struct test_item *retrieved1 = + (struct test_item *)hashtab_get(table, key1_clone, &err); + REQUIRE( retrieved1 == &val1 ); + REQUIRE( errnum(&err) == 0 ); + nput(key1_clone); + } + } + + WHEN( "more items than buckets are inserted" ) + { + auto keys = std::vector(64); + auto vals = std::vector(64); + for (unsigned int i = 0; i < 64; i++) { + auto s = u2nstr(i, 10, nil); + auto k = nbuf_from_nstr(s, nil); + nput(s); + keys[i] = k; + vals[i].number = i; + hashtab_put(table, k, &vals[i], &err); + REQUIRE( errnum(&err) == 0 ); + REQUIRE( nlen(table) == i + 1 ); + } + + THEN( "items can still be retrieved" ) + { + for (unsigned int i = 0; i < 64; i++) { + auto val = (struct test_item *)hashtab_get( + table, + keys[i], + &err + ); + REQUIRE( val == &vals[i] ); + REQUIRE( errnum(&err) == 0 ); + } + REQUIRE( nlen(table) == 64 ); + } + + for (unsigned int i = 0; i < 64; i++) { + nput(keys[i]); + /* + * hashtab_put must have called an additional + * nget on the key, otherwise this would be nil + */ + REQUIRE( keys[i] != nil ); + } + } + + nput(table); + } +} + +/* + * This file is part of libneo. + * Copyright (c) 2021 Fefie . + * + * libneo is non-violent software: you may only use, redistribute, + * and/or modify it under the terms of the CNPLv6+ as found in + * the LICENSE file in the source code root directory or at + * . + * + * libneo comes with ABSOLUTELY NO WARRANTY, to the extent + * permitted by applicable law. See the CNPLv6+ for details. + */