1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2008 Novell, Inc.
4  *
5  * This library is free software: you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation.
8  *
9  * This library is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12  * for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library. If not, see <http://www.gnu.org/licenses/>.
16  *
17  */
18 
19 #include "camel-object-bag.h"
20 
21 #include <glib-object.h>
22 
23 typedef struct _KeyReservation KeyReservation;
24 
25 struct _KeyReservation {
26 	gpointer key;
27 	gint waiters;
28 	GThread *owner;
29 	GCond cond;
30 };
31 
32 struct _CamelObjectBag {
33 	GHashTable *key_table;
34 	GHashTable *object_table;
35 	GEqualFunc key_equal_func;
36 	CamelCopyFunc key_copy_func;
37 	GFreeFunc key_free_func;
38 	GList *reserved;  /* list of KeyReservations */
39 	GMutex mutex;
40 };
41 
42 static KeyReservation *
key_reservation_new(CamelObjectBag * bag,gconstpointer key)43 key_reservation_new (CamelObjectBag *bag,
44                      gconstpointer key)
45 {
46 	KeyReservation *reservation;
47 
48 	reservation = g_slice_new0 (KeyReservation);
49 	reservation->key = bag->key_copy_func (key);
50 	reservation->owner = g_thread_self ();
51 	g_cond_init (&reservation->cond);
52 
53 	bag->reserved = g_list_prepend (bag->reserved, reservation);
54 
55 	return reservation;
56 }
57 
58 static KeyReservation *
key_reservation_lookup(CamelObjectBag * bag,gconstpointer key)59 key_reservation_lookup (CamelObjectBag *bag,
60                         gconstpointer key)
61 {
62 	GList *iter;
63 
64 	/* XXX Might be easier to use a GHashTable for reservations. */
65 	for (iter = bag->reserved; iter != NULL; iter = iter->next) {
66 		KeyReservation *reservation = iter->data;
67 		if (bag->key_equal_func (reservation->key, key))
68 			return reservation;
69 	}
70 
71 	return NULL;
72 }
73 
74 static void
key_reservation_free(CamelObjectBag * bag,KeyReservation * reservation)75 key_reservation_free (CamelObjectBag *bag,
76                       KeyReservation *reservation)
77 {
78 	/* Make sure the reservation is actually in the object bag. */
79 	g_return_if_fail (key_reservation_lookup (bag, reservation->key) != NULL);
80 
81 	bag->reserved = g_list_remove (bag->reserved, reservation);
82 
83 	bag->key_free_func (reservation->key);
84 	g_cond_clear (&reservation->cond);
85 	g_slice_free (KeyReservation, reservation);
86 }
87 
88 static void
object_bag_unreserve(CamelObjectBag * bag,gconstpointer key)89 object_bag_unreserve (CamelObjectBag *bag,
90                       gconstpointer key)
91 {
92 	KeyReservation *reservation;
93 
94 	reservation = key_reservation_lookup (bag, key);
95 	g_return_if_fail (reservation != NULL);
96 	g_return_if_fail (reservation->owner == g_thread_self ());
97 
98 	if (reservation->waiters > 0) {
99 		reservation->owner = NULL;
100 		g_cond_signal (&reservation->cond);
101 	} else
102 		key_reservation_free (bag, reservation);
103 }
104 
105 /* Ick. We need to store the original gobject pointer too, since that's
106  * used as the key in one of the hash tables. So to clean up after the
107  * object dies and the GWeakRef starts returning NULL, we'll need to
108  * know where it *was*. This is safe because we'll always check and
109  * avoid adding a duplicate. But still, ick. */
110 typedef struct {
111 	GWeakRef ref;
112 	gpointer obj;
113 	CamelObjectBag *bag;
114 } ObjRef;
115 
116 static void
object_bag_notify(CamelObjectBag * bag,GObject * where_the_object_was)117 object_bag_notify (CamelObjectBag *bag,
118                    GObject *where_the_object_was)
119 {
120 	gconstpointer key;
121 
122 	g_mutex_lock (&bag->mutex);
123 
124 	key = g_hash_table_lookup (bag->key_table, where_the_object_was);
125 	if (key != NULL) {
126 		g_hash_table_remove (bag->key_table, where_the_object_was);
127 		g_hash_table_remove (bag->object_table, key);
128 	}
129 
130 	g_mutex_unlock (&bag->mutex);
131 }
132 
133 /* Properly destroy an ObjRef as it's freed from the hash table */
134 static void
wref_free_func(gpointer p)135 wref_free_func (gpointer p)
136 {
137 	ObjRef *ref = p;
138 	GObject *obj = g_weak_ref_get (&ref->ref);
139 
140 	if (obj) {
141 		/* The object is being removed from the bag while it's
142 		 * still alive, e.g. by camel_object_bag_remove()
143 		 * or camel_object_bag_destroy(). Drop the weak_ref. */
144 		g_object_weak_unref (
145 			obj, (GWeakNotify) object_bag_notify,
146 			ref->bag);
147 		g_object_unref (obj);
148 	}
149 	g_weak_ref_clear (&ref->ref);
150 	g_slice_free (ObjRef, ref);
151 }
152 
153 /**
154  * camel_object_bag_new: (skip)
155  * @key_hash_func: (scope call): a hashing function for keys
156  * @key_equal_func: (scope call): a comparison function for keys
157  * @key_copy_func: (scope call): a function to copy keys
158  * @key_free_func: (scope call): a function to free keys
159  *
160  * Returns a new object bag.  Object bags are keyed hash tables of objects
161  * that can be updated atomically using transaction semantics.  Use
162  * camel_object_bag_destroy() to free the object bag.
163  *
164  * Returns: a newly-allocated #CamelObjectBag
165  **/
166 CamelObjectBag *
camel_object_bag_new(GHashFunc key_hash_func,GEqualFunc key_equal_func,CamelCopyFunc key_copy_func,GFreeFunc key_free_func)167 camel_object_bag_new (GHashFunc key_hash_func,
168                       GEqualFunc key_equal_func,
169                       CamelCopyFunc key_copy_func,
170                       GFreeFunc key_free_func)
171 {
172 	CamelObjectBag *bag;
173 	GHashTable *key_table;
174 	GHashTable *object_table;
175 
176 	g_return_val_if_fail (key_hash_func != NULL, NULL);
177 	g_return_val_if_fail (key_equal_func != NULL, NULL);
178 	g_return_val_if_fail (key_copy_func != NULL, NULL);
179 	g_return_val_if_fail (key_free_func != NULL, NULL);
180 
181 	/* Each key is shared between both hash tables, so only one
182 	 * table needs to be responsible for destroying keys. */
183 
184 	key_table = g_hash_table_new (g_direct_hash, g_direct_equal);
185 
186 	object_table = g_hash_table_new_full (
187 		key_hash_func, key_equal_func,
188 		(GDestroyNotify) key_free_func,
189 		(GDestroyNotify) wref_free_func);
190 
191 	bag = g_slice_new0 (CamelObjectBag);
192 	bag->key_table = key_table;
193 	bag->object_table = object_table;
194 	bag->key_equal_func = key_equal_func;
195 	bag->key_copy_func = key_copy_func;
196 	bag->key_free_func = key_free_func;
197 	g_mutex_init (&bag->mutex);
198 
199 	return bag;
200 }
201 
202 /**
203  * camel_object_bag_get:
204  * @bag: a #CamelObjectBag
205  * @key: a key
206  *
207  * Lookup an object by @key.  If the key is currently reserved, the function
208  * will block until another thread commits or aborts the reservation.  The
209  * caller owns the reference to the returned object.  Use g_object_unref ()
210  * to unreference it.
211  *
212  * Returns: (transfer full) (nullable): the object corresponding to @key, or
213  * %NULL if not found
214  **/
215 gpointer
camel_object_bag_get(CamelObjectBag * bag,gconstpointer key)216 camel_object_bag_get (CamelObjectBag *bag,
217                       gconstpointer key)
218 {
219 	KeyReservation *reservation;
220 	ObjRef *ref;
221 	gpointer object = NULL;
222 
223 	g_return_val_if_fail (bag != NULL, NULL);
224 	g_return_val_if_fail (key != NULL, NULL);
225 
226 	g_mutex_lock (&bag->mutex);
227 
228 	/* Look for the key in the bag. */
229 	ref = g_hash_table_lookup (bag->object_table, key);
230 	if (ref != NULL) {
231 		object = g_weak_ref_get (&ref->ref);
232 		if (object != NULL) {
233 			g_mutex_unlock (&bag->mutex);
234 			return object;
235 		}
236 
237 		/* Remove stale reference to dead object. */
238 		g_hash_table_remove (bag->key_table, ref->obj);
239 		g_hash_table_remove (bag->object_table, key);
240 	}
241 
242 	/* Check if the key has been reserved. */
243 	reservation = key_reservation_lookup (bag, key);
244 	if (reservation == NULL) {
245 		/* No such key, so return NULL. */
246 		g_mutex_unlock (&bag->mutex);
247 		return NULL;
248 	}
249 
250 	/* Wait for the key to be unreserved. */
251 	reservation->waiters++;
252 	while (reservation->owner != NULL)
253 		g_cond_wait (&reservation->cond, &bag->mutex);
254 	reservation->waiters--;
255 
256 	/* Check if an object was added by another thread. */
257 	ref = g_hash_table_lookup (bag->object_table, key);
258 	if (ref != NULL) {
259 		object = g_weak_ref_get (&ref->ref);
260 		if (object == NULL) {
261 			/* Remove stale reference to dead object. */
262 			g_hash_table_remove (bag->key_table, ref->obj);
263 			g_hash_table_remove (bag->object_table, key);
264 		}
265 	}
266 
267 	/* We're not reserving it. */
268 	reservation->owner = g_thread_self ();
269 	object_bag_unreserve (bag, key);
270 
271 	g_mutex_unlock (&bag->mutex);
272 
273 	return object;
274 }
275 
276 /**
277  * camel_object_bag_peek:
278  * @bag: a #CamelObjectBag
279  * @key: an unreserved key
280  *
281  * Returns the object for @key in @bag, ignoring any reservations.  If it
282  * isn't committed, then it isn't considered.  This should only be used
283  * where reliable transactional-based state is not required.
284  *
285  * Unlink other "peek" operations, the caller owns the returned object
286  * reference.  Use g_object_unref () to unreference it.
287  *
288  * Returns: (transfer full) (nullable): the object for @key, or %NULL if @key
289  * is reserved or not found
290  **/
291 gpointer
camel_object_bag_peek(CamelObjectBag * bag,gconstpointer key)292 camel_object_bag_peek (CamelObjectBag *bag,
293                        gconstpointer key)
294 {
295 	gpointer object = NULL;
296 	ObjRef *ref;
297 
298 	g_return_val_if_fail (bag != NULL, NULL);
299 	g_return_val_if_fail (key != NULL, NULL);
300 
301 	g_mutex_lock (&bag->mutex);
302 
303 	ref = g_hash_table_lookup (bag->object_table, key);
304 	if (ref != NULL)
305 		object = g_weak_ref_get (&ref->ref);
306 
307 	g_mutex_unlock (&bag->mutex);
308 
309 	return object;
310 }
311 
312 /**
313  * camel_object_bag_reserve:
314  * @bag: a #CamelObjectBag
315  * @key: the key to reserve
316  *
317  * Reserves @key in @bag.  If @key is already reserved in another thread,
318  * then wait until the reservation has been committed.
319  *
320  * After reserving @key, you either get a reference to the object
321  * corresponding to @key (similar to camel_object_bag_get()) or you get
322  * %NULL, signifying that you MUST call either camel_object_bag_add() or
323  * camel_object_bag_abort().
324  *
325  * Returns: (transfer full) (nullable): the object for @key, or %NULL if @key
326  * is not found
327  **/
328 gpointer
camel_object_bag_reserve(CamelObjectBag * bag,gconstpointer key)329 camel_object_bag_reserve (CamelObjectBag *bag,
330                           gconstpointer key)
331 {
332 	KeyReservation *reservation;
333 	ObjRef *ref;
334 	gpointer object = NULL;
335 
336 	g_return_val_if_fail (bag != NULL, NULL);
337 	g_return_val_if_fail (key != NULL, NULL);
338 
339 	g_mutex_lock (&bag->mutex);
340 
341 	/* If object for key already exists, return it immediately. */
342 	ref = g_hash_table_lookup (bag->object_table, key);
343 	if (ref != NULL) {
344 		object = g_weak_ref_get (&ref->ref);
345 		if (object != NULL) {
346 			g_mutex_unlock (&bag->mutex);
347 			return object;
348 		}
349 
350 		/* Remove stale reference to dead object. */
351 		g_hash_table_remove (bag->key_table, ref->obj);
352 		g_hash_table_remove (bag->object_table, key);
353 	}
354 
355 	/* If no such key exists in the bag, create a reservation. */
356 	reservation = key_reservation_lookup (bag, key);
357 	if (reservation == NULL) {
358 		key_reservation_new (bag, key);
359 		g_mutex_unlock (&bag->mutex);
360 		return NULL;
361 	}
362 
363 	/* Wait for the reservation to be committed or aborted. */
364 	reservation->waiters++;
365 	while (reservation->owner != NULL)
366 		g_cond_wait (&reservation->cond, &bag->mutex);
367 	reservation->owner = g_thread_self ();
368 	reservation->waiters--;
369 
370 	/* Check if the object was added by another thread. */
371 	ref = g_hash_table_lookup (bag->object_table, key);
372 	if (ref != NULL) {
373 		object = g_weak_ref_get (&ref->ref);
374 		if (object != NULL) {
375 			/* We have an object; no need to reserve the key. */
376 			object_bag_unreserve (bag, key);
377 		} else {
378 			/* Remove stale reference to dead object. */
379 			g_hash_table_remove (bag->key_table, ref->obj);
380 			g_hash_table_remove (bag->object_table, key);
381 		}
382 	}
383 
384 	g_mutex_unlock (&bag->mutex);
385 
386 	return object;
387 }
388 
389 static gboolean
object_in_bag(CamelObjectBag * bag,gpointer object)390 object_in_bag (CamelObjectBag *bag,
391                gpointer object)
392 {
393 	gconstpointer key;
394 	ObjRef *ref;
395 	GObject *obj2;
396 
397 	key = g_hash_table_lookup (bag->key_table, object);
398 	if (key == NULL)
399 		return FALSE;
400 
401 	ref = g_hash_table_lookup (bag->object_table, key);
402 	if (ref == NULL)
403 		return FALSE;
404 
405 	obj2 = g_weak_ref_get (&ref->ref);
406 	if (obj2 == NULL) {
407 		/* Remove stale reference to dead object. */
408 		g_hash_table_remove (bag->key_table, object);
409 		g_hash_table_remove (bag->object_table, key);
410 	} else {
411 		g_object_unref (obj2);
412 	}
413 
414 	return obj2 == object;
415 }
416 
417 /**
418  * camel_object_bag_add:
419  * @bag: a #CamelObjectBag
420  * @key: a reserved key
421  * @object: a #GObject
422  *
423  * Adds @object to @bag.  The @key MUST have been previously reserved using
424  * camel_object_bag_reserve().
425  **/
426 void
camel_object_bag_add(CamelObjectBag * bag,gconstpointer key,gpointer object)427 camel_object_bag_add (CamelObjectBag *bag,
428                       gconstpointer key,
429                       gpointer object)
430 {
431 	g_return_if_fail (bag != NULL);
432 	g_return_if_fail (key != NULL);
433 	g_return_if_fail (G_IS_OBJECT (object));
434 
435 	g_mutex_lock (&bag->mutex);
436 
437 	/* Check it's the *same* object, not an old one at the same address */
438 	if (!object_in_bag (bag, object)) {
439 		ObjRef *ref;
440 		gpointer copied_key;
441 
442 		ref = g_slice_new (ObjRef);
443 		ref->bag = bag;
444 		/* We need to stash a 'raw' pointer since that's the key we use
445 		 * in the key_table */
446 		ref->obj = object;
447 		g_weak_ref_init (&ref->ref, object);
448 		copied_key = bag->key_copy_func (key);
449 		g_hash_table_insert (bag->key_table, object, copied_key);
450 		g_hash_table_insert (bag->object_table, copied_key, ref);
451 		object_bag_unreserve (bag, key);
452 
453 		g_object_weak_ref (
454 			G_OBJECT (object),
455 			(GWeakNotify) object_bag_notify, bag);
456 	}
457 
458 	g_mutex_unlock (&bag->mutex);
459 }
460 
461 /**
462  * camel_object_bag_abort:
463  * @bag: a #CamelObjectBag
464  * @key: a reserved key
465  *
466  * Aborts a key reservation.
467  **/
468 void
camel_object_bag_abort(CamelObjectBag * bag,gconstpointer key)469 camel_object_bag_abort (CamelObjectBag *bag,
470                         gconstpointer key)
471 {
472 	g_return_if_fail (bag != NULL);
473 	g_return_if_fail (key != NULL);
474 
475 	g_mutex_lock (&bag->mutex);
476 
477 	object_bag_unreserve (bag, key);
478 
479 	g_mutex_unlock (&bag->mutex);
480 }
481 
482 /**
483  * camel_object_bag_rekey:
484  * @bag: a #CamelObjectBag
485  * @object: a #GObject
486  * @new_key: a new key for @object
487  *
488  * Changes the key for @object to @new_key, atomically.
489  *
490  * It is considered a programming error if @object is not found in @bag.
491  * In such case the function will emit a terminal warning and return.
492  **/
493 void
camel_object_bag_rekey(CamelObjectBag * bag,gpointer object,gconstpointer new_key)494 camel_object_bag_rekey (CamelObjectBag *bag,
495                         gpointer object,
496                         gconstpointer new_key)
497 {
498 	gpointer key;
499 	ObjRef *ref;
500 
501 	g_return_if_fail (bag != NULL);
502 	g_return_if_fail (G_IS_OBJECT (object));
503 	g_return_if_fail (new_key != NULL);
504 
505 	g_mutex_lock (&bag->mutex);
506 
507 	key = g_hash_table_lookup (bag->key_table, object);
508 	if (key != NULL) {
509 		/* Remove the old key. */
510 		ref = g_hash_table_lookup (bag->object_table, key);
511 		g_hash_table_steal (bag->object_table, key);
512 		g_hash_table_remove (bag->key_table, object);
513 
514 		/* Insert the new key. */
515 		key = bag->key_copy_func (new_key);
516 		g_hash_table_insert (bag->object_table, key, ref);
517 		g_hash_table_insert (bag->key_table, object, key);
518 	} else
519 		g_warn_if_reached ();
520 
521 	g_mutex_unlock (&bag->mutex);
522 }
523 
524 /**
525  * camel_object_bag_list:
526  * @bag: a #CamelObjectBag
527  *
528  * Returns a #GPtrArray of all the objects in the bag.  The caller owns
529  * both the array and the object references, so to free the array use:
530  *
531  * |[
532  *     g_ptr_array_foreach (array, (GFunc) g_object_unref, NULL);
533  *     g_ptr_array_free (array, TRUE);
534  * ]|
535  *
536  * Returns: (element-type GObject) (transfer full): an array of objects in @bag
537  **/
538 GPtrArray *
camel_object_bag_list(CamelObjectBag * bag)539 camel_object_bag_list (CamelObjectBag *bag)
540 {
541 	GPtrArray *array;
542 	GList *values;
543 
544 	g_return_val_if_fail (bag != NULL, NULL);
545 
546 	/* XXX Too bad we're not returning a GList; this would be trivial. */
547 
548 	array = g_ptr_array_new ();
549 
550 	g_mutex_lock (&bag->mutex);
551 
552 	values = g_hash_table_get_values (bag->object_table);
553 	while (values != NULL) {
554 		ObjRef *ref = values->data;
555 		GObject *obj = g_weak_ref_get (&ref->ref);
556 		if (obj)
557 			g_ptr_array_add (array, obj);
558 		values = g_list_delete_link (values, values);
559 	}
560 
561 	g_mutex_unlock (&bag->mutex);
562 
563 	return array;
564 }
565 
566 /**
567  * camel_object_bag_remove:
568  * @bag: a #CamelObjectBag
569  * @object: a #GObject
570  *
571  * Removes @object from @bag.
572  **/
573 void
camel_object_bag_remove(CamelObjectBag * bag,gpointer object)574 camel_object_bag_remove (CamelObjectBag *bag,
575                          gpointer object)
576 {
577 	gpointer key;
578 
579 	g_return_if_fail (bag != NULL);
580 	g_return_if_fail (G_IS_OBJECT (object));
581 
582 	g_mutex_lock (&bag->mutex);
583 
584 	key = g_hash_table_lookup (bag->key_table, object);
585 	if (key != NULL) {
586 		g_hash_table_remove (bag->key_table, object);
587 		g_hash_table_remove (bag->object_table, key);
588 	}
589 
590 	g_mutex_unlock (&bag->mutex);
591 }
592 
593 /**
594  * camel_object_bag_destroy:
595  * @bag: a #CamelObjectBag
596  *
597  * Frees @bag.  As a precaution, the function will emit a warning to standard
598  * error and return without freeing @bag if @bag still has reserved keys.
599  **/
600 void
camel_object_bag_destroy(CamelObjectBag * bag)601 camel_object_bag_destroy (CamelObjectBag *bag)
602 {
603 	g_return_if_fail (bag != NULL);
604 	g_return_if_fail (bag->reserved == NULL);
605 
606 	g_hash_table_destroy (bag->key_table);
607 	g_hash_table_destroy (bag->object_table);
608 	g_mutex_clear (&bag->mutex);
609 	g_slice_free (CamelObjectBag, bag);
610 }
611