640 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			640 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * This file is distributed as part of the MariaDB Corporation MaxScale.  It is free
 | |
|  * software: you can redistribute it and/or modify it under the terms of the
 | |
|  * GNU General Public License as published by the Free Software Foundation,
 | |
|  * version 2.
 | |
|  *
 | |
|  * This program is distributed in the hope that it will be useful, but WITHOUT
 | |
|  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | |
|  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 | |
|  * details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU General Public License along with
 | |
|  * this program; if not, write to the Free Software Foundation, Inc., 51
 | |
|  * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | |
|  *
 | |
|  * Copyright MariaDB Corporation Ab 2013-2014
 | |
|  */
 | |
| #include <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <hashtable.h>
 | |
| 
 | |
| /**
 | |
|  * @file hashtable.c General purpose hashtable routines
 | |
|  *
 | |
|  * The hashtable can be create with a custom number of hash buckets,
 | |
|  * a hash function and optional functions to call make copies of the key
 | |
|  * and value and to free them.
 | |
|  *
 | |
|  * The hashtable is arrange as a set of linked lists, the number of linked
 | |
|  * lists being the hashsize as requested by the user. Entries are hashed by
 | |
|  * calling the hash function that is passed in by the user, this is used as
 | |
|  * an index into the array of linked lists, usign modulo hashsize.
 | |
|  *
 | |
|  * The linked lists are searched using the key comparison function that is
 | |
|  * passed into the hash table creation routine.
 | |
|  *
 | |
|  * By default the hash table keeps the original pointers that are passed in
 | |
|  * for the keys and values, however two functions can be supplied to copy these
 | |
|  * a copy function and a free function. Please note the same function is used for
 | |
|  * the key and the value, if the actions required are different the called functions
 | |
|  * must understand how to differenate the key and value.
 | |
|  *
 | |
|  * The hash table implements a single write, multiple reader locking policy by
 | |
|  * using a pair of counters and a spinlock. The spinlock is used to protect the
 | |
|  * number of readers and writers counters when taking out locks. Releasing of
 | |
|  * locks uses pure atomic actions and thus does not require spinlock protection.
 | |
|  *
 | |
|  * @verbatim
 | |
|  * Revision History
 | |
|  *
 | |
|  * Date		Who			Description
 | |
|  * 23/06/2013	Mark Riddoch		Initial implementation
 | |
|  * 23/07/2013	Mark Riddoch		Addition of hashtable iterator
 | |
|  * 08/01/2014	Massimiliano Pinto	Added copy and free funtion pointers for keys and values:
 | |
|  *					it's possible to copy and free different data types via
 | |
|  *					kcopyfn/kfreefn, vcopyfn/vfreefn
 | |
|  *
 | |
|  * @endverbatim
 | |
|  */
 | |
| 
 | |
| static	void hashtable_read_lock(HASHTABLE *table);
 | |
| static	void hashtable_read_unlock(HASHTABLE *table);
 | |
| static	void hashtable_write_lock(HASHTABLE *table);
 | |
| static	void hashtable_write_unlock(HASHTABLE *table);
 | |
| static HASHTABLE *hashtable_alloc_real(HASHTABLE* target, 
 | |
| 					int size, 
 | |
| 					int (*hashfn)(), 
 | |
| 					int (*cmpfn)());
 | |
| 
 | |
| /**
 | |
|  * Special null function used as default memory allfunctions in the hashtable
 | |
|  * implementation. This avoids having to special case the code that manipulates
 | |
|  * the keys and values
 | |
|  *
 | |
|  * @param	data	The data pointer
 | |
|  * @return	Return the value we were called with
 | |
|  */
 | |
| static void *
 | |
| nullfn(void *data)
 | |
| {
 | |
| 	return data;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Allocate a new hash table.
 | |
|  *
 | |
|  * The hashtable must have a size of at least one, however to be of any
 | |
|  * practical use a larger size sould be chosen as the size relates to the number
 | |
|  * of has buckets in the table.
 | |
|  *
 | |
|  * @param size		The size of the hash table, msut be > 0
 | |
|  * @param hashfn	The user supplied hash function
 | |
|  * @param cmpfn		The user supplied key comparison function
 | |
|  * @return The hashtable table
 | |
|  */
 | |
| HASHTABLE *
 | |
| hashtable_alloc(int size, int (*hashfn)(), int (*cmpfn)())
 | |
| {
 | |
| 	return hashtable_alloc_real(NULL, size, hashfn, cmpfn);
 | |
| }
 | |
| 
 | |
| HASHTABLE* hashtable_alloc_flat(
 | |
| 	HASHTABLE* target, 
 | |
| 	int size, 
 | |
| 	int (*hashfn)(), 
 | |
| 	int (*cmpfn)())
 | |
| {
 | |
| 	return hashtable_alloc_real(target, size, hashfn, cmpfn);
 | |
| }
 | |
| 
 | |
| static HASHTABLE *
 | |
| hashtable_alloc_real(
 | |
| 	HASHTABLE* target, 
 | |
| 	int        size, 
 | |
| 	int (*hashfn)(), 
 | |
| 	int (*cmpfn)())
 | |
| {
 | |
| 	HASHTABLE       *rval;
 | |
| 	
 | |
| 	if (target == NULL)
 | |
| 	{
 | |
| 		if ((rval = malloc(sizeof(HASHTABLE))) == NULL)
 | |
| 			return NULL;
 | |
| 		rval->ht_isflat = false;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		rval = target;
 | |
| 		rval->ht_isflat = true;
 | |
| 	}
 | |
| 	
 | |
| #if defined(SS_DEBUG)
 | |
| 	rval->ht_chk_top = CHK_NUM_HASHTABLE;
 | |
| 	rval->ht_chk_tail = CHK_NUM_HASHTABLE;
 | |
| #endif
 | |
| 	rval->hashsize = size > 0 ? size : 1;
 | |
| 	rval->hashfn = hashfn;
 | |
| 	rval->cmpfn = cmpfn;
 | |
| 	rval->kcopyfn = nullfn;
 | |
| 	rval->vcopyfn = nullfn;
 | |
| 	rval->kfreefn = nullfn;
 | |
| 	rval->vfreefn = nullfn;
 | |
| 	rval->n_readers = 0;
 | |
| 	rval->writelock = 0;
 | |
| 	spinlock_init(&rval->spin);
 | |
| 	if ((rval->entries = (HASHENTRIES **)calloc(rval->hashsize, sizeof(HASHENTRIES *))) == NULL)
 | |
| 	{
 | |
| 		free(rval);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 	memset(rval->entries, 0, rval->hashsize * sizeof(HASHENTRIES *));
 | |
| 
 | |
| 	return rval;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Delete an entire hash table
 | |
|  *
 | |
|  * @param	table	The hash table to delete
 | |
|  */
 | |
| void
 | |
| hashtable_free(HASHTABLE *table)
 | |
| {
 | |
| int		i;
 | |
| HASHENTRIES	*entry, *ptr;
 | |
| 
 | |
| 	hashtable_write_lock(table);
 | |
| 	for (i = 0; i < table->hashsize; i++)
 | |
| 	{
 | |
| 		entry = table->entries[i];
 | |
| 		while (entry)
 | |
| 		{
 | |
| 			ptr = entry->next;
 | |
| 			table->kfreefn(entry->key);
 | |
| 			table->vfreefn(entry->value);
 | |
| 			free(entry);
 | |
| 			entry = ptr;
 | |
| 		}
 | |
| 	}
 | |
| 	free(table->entries);
 | |
| 	
 | |
| 	hashtable_write_unlock(table);
 | |
| 	if (!table->ht_isflat)
 | |
| 	{
 | |
| 		free(table);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Provide memory management functions to the hash table. This allows
 | |
|  * function pointers to be registered that can make copies of the
 | |
|  * key and value and free them as well.
 | |
|  *
 | |
|  * @param table		The hash table
 | |
|  * @param kcopyfn	The copy function for the key
 | |
|  * @param vcopyfn	The copy function for the value
 | |
|  * @param kfreefn	The free function for the key
 | |
|  * @param vfreefn	The free function for the value
 | |
|  */
 | |
| void
 | |
| hashtable_memory_fns(
 | |
| 	HASHTABLE   *table, 
 | |
| 	HASHMEMORYFN kcopyfn, 
 | |
| 	HASHMEMORYFN vcopyfn, 
 | |
| 	HASHMEMORYFN kfreefn, 
 | |
| 	HASHMEMORYFN vfreefn)
 | |
| {
 | |
| 	if (kcopyfn != NULL)
 | |
| 		table->kcopyfn = kcopyfn;
 | |
| 	if (vcopyfn != NULL)
 | |
| 		table->vcopyfn = vcopyfn;
 | |
| 	if (kfreefn != NULL)
 | |
| 		table->kfreefn = kfreefn;
 | |
| 	if (vfreefn != NULL)
 | |
| 		table->vfreefn = vfreefn;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add an item to the hash table.
 | |
|  *
 | |
|  * @param table		The hash table to which to add the item
 | |
|  * @param key		The key of the item
 | |
|  * @param value		The value for the item
 | |
|  * @return	Return the number of items added
 | |
|  */
 | |
| int
 | |
| hashtable_add(HASHTABLE *table, void *key, void *value)
 | |
| {
 | |
|         int		hashkey;
 | |
|         HASHENTRIES	*entry;
 | |
| 
 | |
|         if (key == NULL || value == NULL)
 | |
|             return 0;
 | |
| 
 | |
|         if (table->hashsize <= 0) {            
 | |
|             return 0;
 | |
|         } else {
 | |
|             hashkey = table->hashfn(key) % table->hashsize;
 | |
|         }
 | |
| 	hashtable_write_lock(table);
 | |
| 	entry = table->entries[hashkey % table->hashsize];
 | |
| 	while (entry && table->cmpfn(key, entry->key) != 0)
 | |
| 	{
 | |
| 		entry = entry->next;
 | |
| 	}
 | |
| 	if (entry && table->cmpfn(key, entry->key) == 0)
 | |
| 	{
 | |
| 		/* Duplicate key value */
 | |
| 		hashtable_write_unlock(table);
 | |
| 		return 0;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		HASHENTRIES	*ptr = (HASHENTRIES *)malloc(sizeof(HASHENTRIES));
 | |
| 		if (ptr == NULL)
 | |
| 		{
 | |
| 			hashtable_write_unlock(table);
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		/* copy the key */
 | |
| 		ptr->key = table->kcopyfn(key);
 | |
| 
 | |
| 		/* check succesfull key copy */
 | |
| 		if ( ptr->key  == NULL) {
 | |
| 			free(ptr);
 | |
| 			hashtable_write_unlock(table);
 | |
| 
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		/* copy the value */
 | |
| 		ptr->value = table->vcopyfn(value);
 | |
| 
 | |
| 		/* check succesfull value copy */
 | |
| 		if  ( ptr->value == NULL) {
 | |
| 			/* remove the key ! */
 | |
| 			table->kfreefn(ptr->key);
 | |
| 			free(ptr);
 | |
| 
 | |
| 			/* value not copied, return */
 | |
| 			hashtable_write_unlock(table);
 | |
| 
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		ptr->next = table->entries[hashkey % table->hashsize];
 | |
| 		table->entries[hashkey % table->hashsize] = ptr;
 | |
| 	}
 | |
| 	hashtable_write_unlock(table);
 | |
| 
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Delete an item from the hash table that has a given key
 | |
|  *
 | |
|  * @param table		The hash table to delete from
 | |
|  * @param key		The key value of the item to remove
 | |
|  * @return Return the number of items deleted
 | |
|  */
 | |
| int
 | |
| hashtable_delete(HASHTABLE *table, void *key)
 | |
| {
 | |
| int		hashkey = table->hashfn(key) % table->hashsize;
 | |
| HASHENTRIES	*entry, *ptr;
 | |
| 
 | |
| 	hashtable_write_lock(table);
 | |
| 	entry = table->entries[hashkey % table->hashsize];
 | |
| 	while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
 | |
| 	{
 | |
| 		entry = entry->next;
 | |
| 	}
 | |
| 	if (entry == NULL)
 | |
| 	{
 | |
| 		/* Not found */
 | |
| 		hashtable_write_unlock(table);
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	if (entry == table->entries[hashkey % table->hashsize])
 | |
| 	{
 | |
| 		/* We are removing from the first entry */
 | |
| 		table->entries[hashkey % table->hashsize] = entry->next;
 | |
| 		table->kfreefn(entry->key);
 | |
| 		table->vfreefn(entry->value);
 | |
| 
 | |
|                 if (entry->next != NULL) {
 | |
|                     entry->key = entry->next->key;
 | |
|                     entry->value = entry->next->value;
 | |
|                 } else {
 | |
|                     entry->key = NULL;
 | |
|                     entry->value = NULL;
 | |
|                 }
 | |
| 		free(entry);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		ptr = table->entries[hashkey % table->hashsize];
 | |
| 		while (ptr && ptr->next != entry)
 | |
| 			ptr = ptr->next;
 | |
| 		if (ptr == NULL)
 | |
| 		{
 | |
| 			hashtable_write_unlock(table);
 | |
| 			return 0;	/* This should never happen */
 | |
| 		}
 | |
| 		ptr->next = entry->next;
 | |
| 		table->kfreefn(entry->key);
 | |
| 		table->vfreefn(entry->value);
 | |
| 		free(entry);
 | |
| 	}
 | |
| 	hashtable_write_unlock(table);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Fetch an item with a given key value from the hash table
 | |
|  *
 | |
|  * @param table		The hash table
 | |
|  * @param key		The key value
 | |
|  * @return The item or NULL if the item was not found
 | |
|  */
 | |
| void *
 | |
| hashtable_fetch(HASHTABLE *table, void *key)
 | |
| {
 | |
| int		hashkey = table->hashfn(key) % table->hashsize;
 | |
| HASHENTRIES	*entry;
 | |
| 
 | |
| 	hashtable_read_lock(table);
 | |
| 	entry = table->entries[hashkey % table->hashsize];
 | |
| 	while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
 | |
| 	{
 | |
| 		entry = entry->next;
 | |
| 	}
 | |
| 	if (entry == NULL)
 | |
| 	{
 | |
| 		hashtable_read_unlock(table);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		hashtable_read_unlock(table);
 | |
| 		return entry->value;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Print hash table statistics to the standard output
 | |
|  *
 | |
|  * @param table		The hash table
 | |
|  */
 | |
| void
 | |
| hashtable_stats(HASHTABLE *table)
 | |
| {
 | |
| int		total, longest, i, j;
 | |
| HASHENTRIES	*entries;
 | |
| 
 | |
| 	printf("Hashtable: %p, size %d\n", table, table->hashsize);
 | |
| 	total = 0;
 | |
| 	longest = 0;
 | |
| 	hashtable_read_lock(table);
 | |
| 	for (i = 0; i < table->hashsize; i++)
 | |
| 	{
 | |
| 		j = 0;
 | |
| 		entries = table->entries[i];
 | |
| 		while (entries)
 | |
| 		{
 | |
| 			j++;
 | |
| 			entries = entries->next;
 | |
| 		}
 | |
| 		total += j;
 | |
| 		if (j > longest)
 | |
| 			longest = j;
 | |
| 	}
 | |
| 	hashtable_read_unlock(table);
 | |
| 	printf("\tNo. of entries:     	%d\n", total);
 | |
| 	printf("\tAverage chain length:	%.1f\n", (float)total / table->hashsize);
 | |
| 	printf("\tLongest chain length:	%d\n", longest);
 | |
| }
 | |
| 
 | |
| /** 
 | |
|  * Produces stat output about hashtable 
 | |
|  *
 | |
|  * Parameters:
 | |
|  * @param table - <usage>
 | |
|  *          <description>
 | |
|  *
 | |
|  * @param hashsize - <usage>
 | |
|  *          <description>
 | |
|  *
 | |
|  * @param nelems - <usage>
 | |
|  *          <description>
 | |
|  *
 | |
|  * @param longest - <usage>
 | |
|  *          <description>
 | |
|  *
 | |
|  * @return void
 | |
|  *
 | |
|  *
 | |
|  */
 | |
| void hashtable_get_stats(
 | |
|         void* table,
 | |
|         int*  hashsize,
 | |
|         int*  nelems,
 | |
|         int*  longest)
 | |
| {
 | |
|         HASHTABLE*   ht;
 | |
|         HASHENTRIES* entries;
 | |
|         int          i;
 | |
|         int          j;
 | |
| 
 | |
| 	*nelems = 0;
 | |
| 	*longest = 0;
 | |
| 	*hashsize = 0;
 | |
| 	
 | |
| 	if (table != NULL)
 | |
| 	{
 | |
| 		ht = (HASHTABLE *)table;
 | |
| 		CHK_HASHTABLE(ht);
 | |
| 		hashtable_read_lock(ht);
 | |
| 		
 | |
| 		for (i = 0; i < ht->hashsize; i++)
 | |
| 		{
 | |
| 			j = 0;
 | |
| 			entries = ht->entries[i];
 | |
| 			while (entries)
 | |
| 			{
 | |
| 				j++;
 | |
| 				entries = entries->next;
 | |
| 			}
 | |
| 			*nelems += j;
 | |
| 			if (j > *longest) {
 | |
| 				*longest = j;
 | |
| 			}
 | |
| 		}
 | |
| 		*hashsize = ht->hashsize;
 | |
| 		hashtable_read_unlock(ht);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Take a read lock on the hashtable.
 | |
|  *
 | |
|  * The hashtable support multiple readers and a single writer,
 | |
|  * we have a spinlock to protect the two counts, n_readers and
 | |
|  * writelock.
 | |
|  *
 | |
|  * We take the hashtable spinlock and then check that writelock
 | |
|  * is set to zero. If not we release the spinlock and do dirty
 | |
|  * reads of writelock until it goes to 0. Once it is zero we
 | |
|  * acquire the spinlock again and test that writelock is still
 | |
|  * 0.
 | |
|  *
 | |
|  * With writelock set to zero we increment n_readers with the
 | |
|  * spinlock still held.
 | |
|  *
 | |
|  * @param table		The hashtable to lock.
 | |
|  */
 | |
| static void
 | |
| hashtable_read_lock(HASHTABLE *table)
 | |
| {
 | |
| 	spinlock_acquire(&table->spin);
 | |
| 	while (table->writelock)
 | |
| 	{
 | |
| 		spinlock_release(&table->spin);
 | |
| 		while (table->writelock)
 | |
| 			;
 | |
| 		spinlock_acquire(&table->spin);
 | |
| 	}
 | |
| 	atomic_add(&table->n_readers, 1);
 | |
| 	spinlock_release(&table->spin);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Release a previously obtained readlock.
 | |
|  *
 | |
|  * Simply decrement the n_readers value for the hash table
 | |
|  *
 | |
|  * @param table		The hash table to unlock
 | |
|  */
 | |
| static void
 | |
| hashtable_read_unlock(HASHTABLE *table)
 | |
| {
 | |
| 	atomic_add(&table->n_readers, -1);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Obtain an exclusive write lock for the hash table.
 | |
|  *
 | |
|  * We acquire the hashtable spinlock, check for the number of
 | |
|  * readers beign zero. If it is not we hold the spinlock and
 | |
|  * loop waiting for the n_readers to reach zero. This will prevent
 | |
|  * any new readers beign granted access but will not prevent current
 | |
|  * readers releasing the read lock.
 | |
|  *
 | |
|  * Once we have no readers we increment writelock and test if we are
 | |
|  * the only writelock holder, if not we repeat the process. We hold
 | |
|  * the spinlock throughout the process since both read and write
 | |
|  * locks do not require the spinlock to be acquired.
 | |
|  *
 | |
|  * @param table	The table to lock for updates
 | |
|  */
 | |
| static void
 | |
| hashtable_write_lock(HASHTABLE *table)
 | |
| {
 | |
| int	available;
 | |
| 
 | |
| 	spinlock_acquire(&table->spin);
 | |
| 	do {
 | |
| 		while (table->n_readers)
 | |
| 			;
 | |
| 		available = atomic_add(&table->writelock, 1);
 | |
| 		if (available != 0)
 | |
| 			atomic_add(&table->writelock, -1);
 | |
| 	} while (available != 0);
 | |
| 	spinlock_release(&table->spin);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Release the write lock on the hash table.
 | |
|  *
 | |
|  * @param table The hash table to unlock
 | |
|  */
 | |
| static void
 | |
| hashtable_write_unlock(HASHTABLE *table)
 | |
| {
 | |
| 	atomic_add(&table->writelock, -1);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Create an iterator on a hash table
 | |
|  *
 | |
|  * @param table		The table to ceate an iterator on
 | |
|  * @return	An iterator to use in future calls
 | |
|  */
 | |
| HASHITERATOR *
 | |
| hashtable_iterator(HASHTABLE *table)
 | |
| {
 | |
| HASHITERATOR	*rval;
 | |
| 
 | |
| 	if ((rval = (HASHITERATOR *)malloc(sizeof(HASHITERATOR))) != NULL)
 | |
| 	{
 | |
| 		rval->table = table;
 | |
| 		rval->chain = 0;
 | |
| 		rval->depth = -1;
 | |
| 	}
 | |
| 	return rval;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Return the next key for a hashtable iterator
 | |
|  *
 | |
|  * @param iter	The hashtable iterator
 | |
|  * @return	The next key value or NULL
 | |
|  */
 | |
| void *
 | |
| hashtable_next(HASHITERATOR *iter)
 | |
| {
 | |
| int		i;
 | |
| HASHENTRIES	*entries;
 | |
| 
 | |
| 	iter->depth++;
 | |
| 	while (iter->chain < iter->table->hashsize)
 | |
| 	{
 | |
| 		hashtable_read_lock(iter->table);
 | |
| 		if ((entries = iter->table->entries[iter->chain]) != NULL)
 | |
| 		{
 | |
| 			i = 0;
 | |
| 			while (entries && i < iter->depth)
 | |
| 			{
 | |
| 				entries = entries->next;
 | |
| 				i++;
 | |
| 			}
 | |
| 			hashtable_read_unlock(iter->table);
 | |
| 			if (entries)
 | |
| 				return entries->key;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			hashtable_read_unlock(iter->table);
 | |
| 		}
 | |
| 		iter->depth = 0;
 | |
| 		iter->chain++;
 | |
| 	}
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Free a hashtable iterator
 | |
|  *
 | |
|  * @param iter	The iterator to free
 | |
|  */
 | |
| void
 | |
| hashtable_iterator_free(HASHITERATOR *iter)
 | |
| {
 | |
| 	free(iter);
 | |
| }
 | 
