/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2008 Novell, Inc.
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* This library 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*
*/
#include "camel-object-bag.h"
#include
typedef struct _KeyReservation KeyReservation;
struct _KeyReservation {
gpointer key;
gint waiters;
GThread *owner;
GCond cond;
};
struct _CamelObjectBag {
GHashTable *key_table;
GHashTable *object_table;
GEqualFunc key_equal_func;
CamelCopyFunc key_copy_func;
GFreeFunc key_free_func;
GList *reserved; /* list of KeyReservations */
GMutex mutex;
};
static KeyReservation *
key_reservation_new (CamelObjectBag *bag,
gconstpointer key)
{
KeyReservation *reservation;
reservation = g_slice_new0 (KeyReservation);
reservation->key = bag->key_copy_func (key);
reservation->owner = g_thread_self ();
g_cond_init (&reservation->cond);
bag->reserved = g_list_prepend (bag->reserved, reservation);
return reservation;
}
static KeyReservation *
key_reservation_lookup (CamelObjectBag *bag,
gconstpointer key)
{
GList *iter;
/* XXX Might be easier to use a GHashTable for reservations. */
for (iter = bag->reserved; iter != NULL; iter = iter->next) {
KeyReservation *reservation = iter->data;
if (bag->key_equal_func (reservation->key, key))
return reservation;
}
return NULL;
}
static void
key_reservation_free (CamelObjectBag *bag,
KeyReservation *reservation)
{
/* Make sure the reservation is actually in the object bag. */
g_return_if_fail (key_reservation_lookup (bag, reservation->key) != NULL);
bag->reserved = g_list_remove (bag->reserved, reservation);
bag->key_free_func (reservation->key);
g_cond_clear (&reservation->cond);
g_slice_free (KeyReservation, reservation);
}
static void
object_bag_unreserve (CamelObjectBag *bag,
gconstpointer key)
{
KeyReservation *reservation;
reservation = key_reservation_lookup (bag, key);
g_return_if_fail (reservation != NULL);
g_return_if_fail (reservation->owner == g_thread_self ());
if (reservation->waiters > 0) {
reservation->owner = NULL;
g_cond_signal (&reservation->cond);
} else
key_reservation_free (bag, reservation);
}
/* Ick. We need to store the original gobject pointer too, since that's
* used as the key in one of the hash tables. So to clean up after the
* object dies and the GWeakRef starts returning NULL, we'll need to
* know where it *was*. This is safe because we'll always check and
* avoid adding a duplicate. But still, ick. */
typedef struct {
GWeakRef ref;
gpointer obj;
CamelObjectBag *bag;
} ObjRef;
static void
object_bag_notify (CamelObjectBag *bag,
GObject *where_the_object_was)
{
gconstpointer key;
g_mutex_lock (&bag->mutex);
key = g_hash_table_lookup (bag->key_table, where_the_object_was);
if (key != NULL) {
g_hash_table_remove (bag->key_table, where_the_object_was);
g_hash_table_remove (bag->object_table, key);
}
g_mutex_unlock (&bag->mutex);
}
/* Properly destroy an ObjRef as it's freed from the hash table */
static void
wref_free_func (gpointer p)
{
ObjRef *ref = p;
GObject *obj = g_weak_ref_get (&ref->ref);
if (obj) {
/* The object is being removed from the bag while it's
* still alive, e.g. by camel_object_bag_remove()
* or camel_object_bag_destroy(). Drop the weak_ref. */
g_object_weak_unref (
obj, (GWeakNotify) object_bag_notify,
ref->bag);
g_object_unref (obj);
}
g_weak_ref_clear (&ref->ref);
g_slice_free (ObjRef, ref);
}
/**
* camel_object_bag_new:
* @key_hash_func: a hashing function for keys
* @key_equal_func: a comparison function for keys
* @key_copy_func: a function to copy keys
* @key_free_func: a function to free keys
*
* Returns a new object bag. Object bags are keyed hash tables of objects
* that can be updated atomically using transaction semantics. Use
* camel_object_bag_destroy() to free the object bag.
*
* Returns: a newly-allocated #CamelObjectBag
**/
CamelObjectBag *
camel_object_bag_new (GHashFunc key_hash_func,
GEqualFunc key_equal_func,
CamelCopyFunc key_copy_func,
GFreeFunc key_free_func)
{
CamelObjectBag *bag;
GHashTable *key_table;
GHashTable *object_table;
g_return_val_if_fail (key_hash_func != NULL, NULL);
g_return_val_if_fail (key_equal_func != NULL, NULL);
g_return_val_if_fail (key_copy_func != NULL, NULL);
g_return_val_if_fail (key_free_func != NULL, NULL);
/* Each key is shared between both hash tables, so only one
* table needs to be responsible for destroying keys. */
key_table = g_hash_table_new (g_direct_hash, g_direct_equal);
object_table = g_hash_table_new_full (
key_hash_func, key_equal_func,
(GDestroyNotify) key_free_func,
(GDestroyNotify) wref_free_func);
bag = g_slice_new0 (CamelObjectBag);
bag->key_table = key_table;
bag->object_table = object_table;
bag->key_equal_func = key_equal_func;
bag->key_copy_func = key_copy_func;
bag->key_free_func = key_free_func;
g_mutex_init (&bag->mutex);
return bag;
}
/**
* camel_object_bag_get:
* @bag: a #CamelObjectBag
* @key: a key
*
* Lookup an object by @key. If the key is currently reserved, the function
* will block until another thread commits or aborts the reservation. The
* caller owns the reference to the returned object. Use g_object_unref ()
* to unreference it.
*
* Returns: the object corresponding to @key, or %NULL if not found
**/
gpointer
camel_object_bag_get (CamelObjectBag *bag,
gconstpointer key)
{
KeyReservation *reservation;
ObjRef *ref;
gpointer object = NULL;
g_return_val_if_fail (bag != NULL, NULL);
g_return_val_if_fail (key != NULL, NULL);
g_mutex_lock (&bag->mutex);
/* Look for the key in the bag. */
ref = g_hash_table_lookup (bag->object_table, key);
if (ref != NULL) {
object = g_weak_ref_get (&ref->ref);
if (object != NULL) {
g_mutex_unlock (&bag->mutex);
return object;
}
/* Remove stale reference to dead object. */
g_hash_table_remove (bag->key_table, ref->obj);
g_hash_table_remove (bag->object_table, key);
}
/* Check if the key has been reserved. */
reservation = key_reservation_lookup (bag, key);
if (reservation == NULL) {
/* No such key, so return NULL. */
g_mutex_unlock (&bag->mutex);
return NULL;
}
/* Wait for the key to be unreserved. */
reservation->waiters++;
while (reservation->owner != NULL)
g_cond_wait (&reservation->cond, &bag->mutex);
reservation->waiters--;
/* Check if an object was added by another thread. */
ref = g_hash_table_lookup (bag->object_table, key);
if (ref != NULL) {
object = g_weak_ref_get (&ref->ref);
if (object == NULL) {
/* Remove stale reference to dead object. */
g_hash_table_remove (bag->key_table, ref->obj);
g_hash_table_remove (bag->object_table, key);
}
}
/* We're not reserving it. */
reservation->owner = g_thread_self ();
object_bag_unreserve (bag, key);
g_mutex_unlock (&bag->mutex);
return object;
}
/**
* camel_object_bag_peek:
* @bag: a #CamelObjectBag
* @key: an unreserved key
*
* Returns the object for @key in @bag, ignoring any reservations. If it
* isn't committed, then it isn't considered. This should only be used
* where reliable transactional-based state is not required.
*
* Unlink other "peek" operations, the caller owns the returned object
* reference. Use g_object_unref () to unreference it.
*
* Returns: the object for @key, or %NULL if @key is reserved or not found
**/
gpointer
camel_object_bag_peek (CamelObjectBag *bag,
gconstpointer key)
{
gpointer object = NULL;
ObjRef *ref;
g_return_val_if_fail (bag != NULL, NULL);
g_return_val_if_fail (key != NULL, NULL);
g_mutex_lock (&bag->mutex);
ref = g_hash_table_lookup (bag->object_table, key);
if (ref != NULL)
object = g_weak_ref_get (&ref->ref);
g_mutex_unlock (&bag->mutex);
return object;
}
/**
* camel_object_bag_reserve:
* @bag: a #CamelObjectBag
* @key: the key to reserve
*
* Reserves @key in @bag. If @key is already reserved in another thread,
* then wait until the reservation has been committed.
*
* After reserving @key, you either get a reference to the object
* corresponding to @key (similar to camel_object_bag_get()) or you get
* %NULL, signifying that you MUST call either camel_object_bag_add() or
* camel_object_bag_abort().
*
* Returns: the object for @key, or %NULL if @key is not found
**/
gpointer
camel_object_bag_reserve (CamelObjectBag *bag,
gconstpointer key)
{
KeyReservation *reservation;
ObjRef *ref;
gpointer object = NULL;
g_return_val_if_fail (bag != NULL, NULL);
g_return_val_if_fail (key != NULL, NULL);
g_mutex_lock (&bag->mutex);
/* If object for key already exists, return it immediately. */
ref = g_hash_table_lookup (bag->object_table, key);
if (ref != NULL) {
object = g_weak_ref_get (&ref->ref);
if (object != NULL) {
g_mutex_unlock (&bag->mutex);
return object;
}
/* Remove stale reference to dead object. */
g_hash_table_remove (bag->key_table, ref->obj);
g_hash_table_remove (bag->object_table, key);
}
/* If no such key exists in the bag, create a reservation. */
reservation = key_reservation_lookup (bag, key);
if (reservation == NULL) {
key_reservation_new (bag, key);
g_mutex_unlock (&bag->mutex);
return NULL;
}
/* Wait for the reservation to be committed or aborted. */
reservation->waiters++;
while (reservation->owner != NULL)
g_cond_wait (&reservation->cond, &bag->mutex);
reservation->owner = g_thread_self ();
reservation->waiters--;
/* Check if the object was added by another thread. */
ref = g_hash_table_lookup (bag->object_table, key);
if (ref != NULL) {
object = g_weak_ref_get (&ref->ref);
if (object != NULL) {
/* We have an object; no need to reserve the key. */
object_bag_unreserve (bag, key);
} else {
/* Remove stale reference to dead object. */
g_hash_table_remove (bag->key_table, ref->obj);
g_hash_table_remove (bag->object_table, key);
}
}
g_mutex_unlock (&bag->mutex);
return object;
}
static gboolean
object_in_bag (CamelObjectBag *bag,
gpointer object)
{
gconstpointer key;
ObjRef *ref;
GObject *obj2;
key = g_hash_table_lookup (bag->key_table, object);
if (key == NULL)
return FALSE;
ref = g_hash_table_lookup (bag->object_table, key);
if (ref == NULL)
return FALSE;
obj2 = g_weak_ref_get (&ref->ref);
if (obj2 == NULL) {
/* Remove stale reference to dead object. */
g_hash_table_remove (bag->key_table, object);
g_hash_table_remove (bag->object_table, key);
} else {
g_object_unref (obj2);
}
return obj2 == object;
}
/**
* camel_object_bag_add:
* @bag: a #CamelObjectBag
* @key: a reserved key
* @object: a #GObject
*
* Adds @object to @bag. The @key MUST have been previously reserved using
* camel_object_bag_reserve().
**/
void
camel_object_bag_add (CamelObjectBag *bag,
gconstpointer key,
gpointer object)
{
g_return_if_fail (bag != NULL);
g_return_if_fail (key != NULL);
g_return_if_fail (G_IS_OBJECT (object));
g_mutex_lock (&bag->mutex);
/* Check it's the *same* object, not an old one at the same address */
if (!object_in_bag (bag, object)) {
ObjRef *ref;
gpointer copied_key;
ref = g_slice_new (ObjRef);
ref->bag = bag;
/* We need to stash a 'raw' pointer since that's the key we use
* in the key_table */
ref->obj = object;
g_weak_ref_init (&ref->ref, object);
copied_key = bag->key_copy_func (key);
g_hash_table_insert (bag->key_table, object, copied_key);
g_hash_table_insert (bag->object_table, copied_key, ref);
object_bag_unreserve (bag, key);
g_object_weak_ref (
G_OBJECT (object),
(GWeakNotify) object_bag_notify, bag);
}
g_mutex_unlock (&bag->mutex);
}
/**
* camel_object_bag_abort:
* @bag: a #CamelObjectBag
* @key: a reserved key
*
* Aborts a key reservation.
**/
void
camel_object_bag_abort (CamelObjectBag *bag,
gconstpointer key)
{
g_return_if_fail (bag != NULL);
g_return_if_fail (key != NULL);
g_mutex_lock (&bag->mutex);
object_bag_unreserve (bag, key);
g_mutex_unlock (&bag->mutex);
}
/**
* camel_object_bag_rekey:
* @bag: a #CamelObjectBag
* @object: a #GObject
* @new_key: a new key for @object
*
* Changes the key for @object to @new_key, atomically.
*
* It is considered a programming error if @object is not found in @bag.
* In such case the function will emit a terminal warning and return.
**/
void
camel_object_bag_rekey (CamelObjectBag *bag,
gpointer object,
gconstpointer new_key)
{
gpointer key;
ObjRef *ref;
g_return_if_fail (bag != NULL);
g_return_if_fail (G_IS_OBJECT (object));
g_return_if_fail (new_key != NULL);
g_mutex_lock (&bag->mutex);
key = g_hash_table_lookup (bag->key_table, object);
if (key != NULL) {
/* Remove the old key. */
ref = g_hash_table_lookup (bag->object_table, key);
g_hash_table_steal (bag->object_table, key);
g_hash_table_remove (bag->key_table, object);
/* Insert the new key. */
key = bag->key_copy_func (new_key);
g_hash_table_insert (bag->object_table, key, ref);
g_hash_table_insert (bag->key_table, object, key);
} else
g_warn_if_reached ();
g_mutex_unlock (&bag->mutex);
}
/**
* camel_object_bag_list:
* @bag: a #CamelObjectBag
*
* Returns a #GPtrArray of all the objects in the bag. The caller owns
* both the array and the object references, so to free the array use:
*
*
*
* g_ptr_array_foreach (array, g_object_unref, NULL);
* g_ptr_array_free (array, TRUE);
*
*
*
* Returns: an array of objects in @bag
**/
GPtrArray *
camel_object_bag_list (CamelObjectBag *bag)
{
GPtrArray *array;
GList *values;
g_return_val_if_fail (bag != NULL, NULL);
/* XXX Too bad we're not returning a GList; this would be trivial. */
array = g_ptr_array_new ();
g_mutex_lock (&bag->mutex);
values = g_hash_table_get_values (bag->object_table);
while (values != NULL) {
ObjRef *ref = values->data;
GObject *obj = g_weak_ref_get (&ref->ref);
if (obj)
g_ptr_array_add (array, obj);
values = g_list_delete_link (values, values);
}
g_mutex_unlock (&bag->mutex);
return array;
}
/**
* camel_object_bag_remove:
* @bag: a #CamelObjectBag
* @object: a #GObject
*
* Removes @object from @bag.
**/
void
camel_object_bag_remove (CamelObjectBag *bag,
gpointer object)
{
gpointer key;
g_return_if_fail (bag != NULL);
g_return_if_fail (G_IS_OBJECT (object));
g_mutex_lock (&bag->mutex);
key = g_hash_table_lookup (bag->key_table, object);
if (key != NULL) {
g_hash_table_remove (bag->key_table, object);
g_hash_table_remove (bag->object_table, key);
}
g_mutex_unlock (&bag->mutex);
}
/**
* camel_object_bag_destroy:
* @bag: a #CamelObjectBag
*
* Frees @bag. As a precaution, the function will emit a warning to standard
* error and return without freeing @bag if @bag still has reserved keys.
**/
void
camel_object_bag_destroy (CamelObjectBag *bag)
{
g_return_if_fail (bag != NULL);
g_return_if_fail (bag->reserved == NULL);
g_hash_table_destroy (bag->key_table);
g_hash_table_destroy (bag->object_table);
g_mutex_clear (&bag->mutex);
g_slice_free (CamelObjectBag, bag);
}