Re: [AD] user-defined events

[ Thread Index | Date Index | More lists.liballeg.org/allegro-developers Archives ]


On 2008-10-16, Elias Pschernig <elias.pschernig@xxxxxxxxxx> wrote:
> On Fri, 2008-10-17 at 00:06 +1100, Peter Wang wrote:
> > 
> > When is an event supposed to be destroyed?  al_wait_for_event() returns a
> > copy of the event, probably on the user's stack.  Allegro doesn't know
> > when that copy is no longer needed, so the user needs to explicitly call
> > an unref function.
> 
> My idea was, Allegro calls the destroy_event() callback whenever it
> loses a (not necessarily the last) reference to an event. Because it
> calls the copy_event() callback whenever a copy is made, it should be
> 100% correct - users can do their own reference counting inside the
> copy/destroy callbacks, or make actual copies of the user data and free
> them. Is it not going to work?

I can't see how it would work.  Probably I didn't understand your
description.

> > Implementing reference counting for user events wouldn't actually be
> > hard, but I'm still unsure.  If you use atomic instructions then you
> > would at least avoid having an extra mutex per event.  Maybe do it as an
> > exercise and see?
> > 
> 
> Yes, likely to call the callbacks at the correct location, it would be
> as much effort as implementing reference counting - haven't looked at
> the code yet.
> 
> > Or use a conservative garbage collector..
> > 
> 
> I wonder if that would be easy, if I want to only collect my user events
> - seems overkill in any case.

Definitely overkill :)

The following patch implements (optional) reference counting for user
events.  al_emit_user_event() takes a destructor argument,
and the user needs to call al_unref_user_event().

Peter
diff --git a/addons/acodec/ogg.c b/addons/acodec/ogg.c
index fa5a949..9f77196 100644
--- a/addons/acodec/ogg.c
+++ b/addons/acodec/ogg.c
@@ -120,7 +120,7 @@ static void ogg_stream_close(ALLEGRO_STREAM *stream)
    ALLEGRO_EVENT quit_event;
 
    quit_event.type = _KCM_STREAM_FEEDER_QUIT_EVENT_TYPE;
-   al_emit_user_event(*((ALLEGRO_EVENT_SOURCE**)stream), &quit_event);
+   al_emit_user_event(*((ALLEGRO_EVENT_SOURCE**)stream), &quit_event, NULL);
    al_join_thread(stream->feed_thread, NULL);
    al_destroy_thread(stream->feed_thread);
 
diff --git a/addons/acodec/wav.c b/addons/acodec/wav.c
index b65c353..cab86f3 100644
--- a/addons/acodec/wav.c
+++ b/addons/acodec/wav.c
@@ -160,7 +160,7 @@ static void _sndfile_stream_close(ALLEGRO_STREAM *stream)
    ALLEGRO_EVENT quit_event;
 
    quit_event.type = _KCM_STREAM_FEEDER_QUIT_EVENT_TYPE;
-   al_emit_user_event(*((ALLEGRO_EVENT_SOURCE**)stream), &quit_event);
+   al_emit_user_event(*((ALLEGRO_EVENT_SOURCE**)stream), &quit_event, NULL);
    al_join_thread(stream->feed_thread, NULL);
    al_destroy_thread(stream->feed_thread);
 
diff --git a/addons/kcm_audio/kcm_stream.c b/addons/kcm_audio/kcm_stream.c
index 6cb01e2..6dd1dba 100644
--- a/addons/kcm_audio/kcm_stream.c
+++ b/addons/kcm_audio/kcm_stream.c
@@ -608,7 +608,7 @@ bool _al_kcm_emit_stream_event(ALLEGRO_STREAM *stream, unsigned long count)
       ALLEGRO_EVENT event;
       event.user.type = ALLEGRO_EVENT_STREAM_EMPTY_FRAGMENT;
       event.user.timestamp = al_current_time();
-      al_emit_user_event(stream->spl.es, &event);
+      al_emit_user_event(stream->spl.es, &event, NULL);
    }
 
    return true;
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 05c01ca..6039855 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -66,6 +66,7 @@ example(ex_timedwait NOTHING)
 example(ex_get_path NOTHING)
 example(ex_path NOTHING)
 example(ex_user_events NOTHING)
+example(ex_user_events2 NOTHING)
 
 if(SUPPORT_D3D)
     example_cxx(ex_d3d d3dx9)
diff --git a/examples/ex_user_events.c b/examples/ex_user_events.c
index bbd0c52..4b1ecbc 100644
--- a/examples/ex_user_events.c
+++ b/examples/ex_user_events.c
@@ -7,7 +7,38 @@
 
 
 /* XXX Currently we have no formal way to allocate event type numbers. */
-#define MY_EVENT_TYPE   1025
+#define MY_SIMPLE_EVENT_TYPE     1025
+#define MY_COMPLEX_EVENT_TYPE    1026
+
+
+/* Just some fantasy event, supposedly used in an RPG - it's just to show that
+ * in practice, the 4 user fields we have now never will be enough. */
+typedef struct MY_EVENT
+{
+   int id;
+   int type; /* For example "attack" or "buy". */
+   int x, y, z; /* Position in the game world the event takes place. */
+   int server_time; /* Game time in ticks the event takes place. */
+   int source_unit_id; /* E.g. attacker or seller. */
+   int destination_unit_id; /* E.g. defender of buyer. */
+   int item_id; /* E.g. weapon used or item sold. */
+   int amount; /* Gold the item is sold for. */
+} MY_EVENT;
+
+
+MY_EVENT *new_event(int id)
+{
+    MY_EVENT *event = calloc(1, sizeof *event);
+    event->id = id;
+    return event;
+}
+
+
+void my_event_dtor(ALLEGRO_USER_EVENT *event)
+{
+   printf("my_event_dtor: %p\n", (void *) event->data1);
+   free((void *) event->data1);
+}
 
 
 int main(void)
@@ -49,21 +80,35 @@ int main(void)
 
          printf("Got timer event %d\n", n);
 
-         user_event.user.type = MY_EVENT_TYPE;
+         user_event.user.type = MY_SIMPLE_EVENT_TYPE;
          user_event.user.data1 = n;
-         al_emit_user_event(user_src, &user_event);
+         al_emit_user_event(user_src, &user_event, NULL);
+
+         user_event.user.type = MY_COMPLEX_EVENT_TYPE;
+         user_event.user.data1 = (intptr_t)new_event(n);
+         al_emit_user_event(user_src, &user_event, my_event_dtor);
       }
-      else if (event.type == MY_EVENT_TYPE) {
+      else if (event.type == MY_SIMPLE_EVENT_TYPE) {
          int n = (int) event.user.data1;
          ASSERT(event.user.source == user_src);
 
-         printf("Got user event %d\n", n);
+         al_unref_user_event(&event.user);
+
+         printf("Got simple user event %d\n", n);
          if (n == 5) {
             break;
          }
       }
+      else if (event.type == MY_COMPLEX_EVENT_TYPE) {
+         MY_EVENT *my_event = (void *)event.user.data1;
+         ASSERT(event.user.source == user_src);
+
+         printf("Got complex user event %d\n", my_event->id);
+         al_unref_user_event(&event.user);
+      }
    }
 
+   al_destroy_event_queue(queue);
    al_destroy_user_event_source(user_src);
    al_uninstall_timer(timer);
 
diff --git a/examples/ex_user_events2.c b/examples/ex_user_events2.c
new file mode 100644
index 0000000..c55d956
--- /dev/null
+++ b/examples/ex_user_events2.c
@@ -0,0 +1,70 @@
+/*
+ *    Example program for the Allegro library.
+ */
+
+#include <stdio.h>
+#include "allegro5/allegro5.h"
+
+
+/* XXX Currently we have no formal way to allocate event type numbers. */
+#define MY_EVENT_TYPE   1025
+
+
+void print_dtor(ALLEGRO_USER_EVENT *event)
+{
+   printf("print_dtor called\n");
+   (void) event;
+}
+
+
+int main(void)
+{
+   ALLEGRO_EVENT_SOURCE *user_src;
+   ALLEGRO_EVENT_QUEUE *queue;
+   ALLEGRO_EVENT user_event;
+   ALLEGRO_EVENT event;
+
+   if (!al_init()) {
+      TRACE("Could not init Allegro.\n");
+      return 1;
+   }
+
+   user_src = al_create_user_event_source();
+   if (!user_src) {
+      TRACE("Could not create user event source.\n");
+      return 1;
+   }
+
+   queue = al_create_event_queue();
+   al_register_event_source(queue, user_src);
+
+   user_event.type = MY_EVENT_TYPE;
+
+   al_emit_user_event(user_src, &user_event, print_dtor);
+   printf("peek: print_dtor should NOT be called\n");
+   al_peek_next_event(queue, &user_event);
+   al_unref_user_event(&user_event.user);
+
+   printf("get: print_dtor should be called \n");
+   al_get_next_event(queue, &user_event);
+   al_unref_user_event(&user_event.user);
+
+   al_emit_user_event(user_src, &user_event, print_dtor);
+   printf("drop: print_dtor should be called\n");
+   al_drop_next_event(queue);
+
+   al_emit_user_event(user_src, &user_event, print_dtor);
+   printf("flush: print_dtor should be called\n");
+   al_flush_event_queue(queue);
+
+   al_emit_user_event(user_src, &user_event, print_dtor);
+   printf("destroy_queue: print_dtor should be called\n");
+   al_destroy_event_queue(queue);
+
+   al_destroy_user_event_source(user_src);
+
+   return 0;
+}
+END_OF_MAIN()
+
+/* vim: set sts=3 sw=3 et: */
diff --git a/include/allegro5/events.h b/include/allegro5/events.h
index 5e03bd0..a9dd166 100644
--- a/include/allegro5/events.h
+++ b/include/allegro5/events.h
@@ -131,6 +131,8 @@ enum
    ALLEGRO_EVENT_DISPLAY_SWITCH_OUT          = 46
 };
 
+#define ALLEGRO_EVENT_TYPE_IS_USER(t)        ((t) >= 1024)
+
 
 
 /*
@@ -218,6 +220,7 @@ typedef struct ALLEGRO_TIMER_EVENT
 typedef struct ALLEGRO_USER_EVENT
 {
    _AL_EVENT_HEADER(struct ALLEGRO_EVENT_SOURCE)
+   struct ALLEGRO_USER_EVENT_DESCRIPTOR *__internal__descr;
    intptr_t data1;
    intptr_t data2;
    intptr_t data3;
@@ -284,7 +287,9 @@ AL_FUNC(void, al_destroy_user_event_source, (ALLEGRO_EVENT_SOURCE *));
 /* The second argument is ALLEGRO_EVENT instead of ALLEGRO_USER_EVENT
  * to prevent users passing a pointer to a too-short structure.
  */
-AL_FUNC(bool, al_emit_user_event, (ALLEGRO_EVENT_SOURCE *, ALLEGRO_EVENT *));
+AL_FUNC(bool, al_emit_user_event, (ALLEGRO_EVENT_SOURCE *, ALLEGRO_EVENT *,
+                                   void (*dtor)(ALLEGRO_USER_EVENT *)));
+AL_FUNC(void, al_unref_user_event, (ALLEGRO_USER_EVENT *));
 
 
 
diff --git a/include/allegro5/internal/aintern_events.h b/include/allegro5/internal/aintern_events.h
index bbfe272..c39b999 100644
--- a/include/allegro5/internal/aintern_events.h
+++ b/include/allegro5/internal/aintern_events.h
@@ -15,6 +15,12 @@ struct ALLEGRO_EVENT_SOURCE
    _AL_VECTOR queues;
 };
 
+typedef struct ALLEGRO_USER_EVENT_DESCRIPTOR
+{
+   void (*dtor)(ALLEGRO_USER_EVENT *event);
+   volatile int refcount;
+} ALLEGRO_USER_EVENT_DESCRIPTOR;
+
 
 AL_FUNC(void, _al_event_source_init, (ALLEGRO_EVENT_SOURCE*));
 AL_FUNC(void, _al_event_source_free, (ALLEGRO_EVENT_SOURCE*));
diff --git a/src/events.c b/src/events.c
index d7b7c8b..f07f1d2 100644
--- a/src/events.c
+++ b/src/events.c
@@ -48,6 +48,8 @@ struct ALLEGRO_EVENT_QUEUE
 static bool do_wait_for_event(ALLEGRO_EVENT_QUEUE *queue,
    ALLEGRO_EVENT *ret_event, ALLEGRO_TIMEOUT *timeout);
 static void copy_event(ALLEGRO_EVENT *dest, const ALLEGRO_EVENT *src);
+static void ref_if_user_event(ALLEGRO_EVENT *event);
+static void unref_if_user_event(ALLEGRO_EVENT *event);
 static void discard_events_of_source(ALLEGRO_EVENT_QUEUE *queue,
    const ALLEGRO_EVENT_SOURCE *source);
 
@@ -186,7 +188,7 @@ bool al_event_queue_is_empty(ALLEGRO_EVENT_QUEUE *queue)
 /* circ_array_next:
  *  Return the next index in a circular array.
  */
-static int circ_array_next(const _AL_VECTOR *vector, int i)
+static unsigned int circ_array_next(const _AL_VECTOR *vector, unsigned int i)
 {
    return (i + 1) % _al_vector_size(vector);
 }
@@ -218,22 +220,24 @@ static ALLEGRO_EVENT *get_next_event_if_any(ALLEGRO_EVENT_QUEUE *queue,
 
 
 
-/* get_peek_or_drop_next_event: [primary thread]
- *  Helper function to do the work for al_get_next_event,
- *  al_peek_next_event and al_drop_next_event which are all very
- *  similar.
+/* Function: al_get_next_event
+ *  Take the next event packet out of the event queue specified, and
+ *  copy the contents into RET_EVENT, returning true.  The original
+ *  event packet will be removed from the queue.  If the event queue is
+ *  empty, return false and the contents of RET_EVENT are unspecified.
  */
-static bool get_peek_or_drop_next_event(ALLEGRO_EVENT_QUEUE *queue,
-   ALLEGRO_EVENT *do_copy, bool delete)
+bool al_get_next_event(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_EVENT *ret_event)
 {
    ALLEGRO_EVENT *next_event;
+   ASSERT(queue);
+   ASSERT(ret_event);
 
    _al_mutex_lock(&queue->mutex);
 
-   next_event = get_next_event_if_any(queue, delete);
+   next_event = get_next_event_if_any(queue, true);
    if (next_event) {
-      if (do_copy)
-         copy_event(do_copy, next_event);
+      copy_event(ret_event, next_event);
+      /* Don't increment reference count on user events. */
    }
 
    _al_mutex_unlock(&queue->mutex);
@@ -243,22 +247,6 @@ static bool get_peek_or_drop_next_event(ALLEGRO_EVENT_QUEUE *queue,
 
 
 
-/* Function: al_get_next_event
- *  Take the next event packet out of the event queue specified, and
- *  copy the contents into RET_EVENT, returning true.  The original
- *  event packet will be removed from the queue.  If the event queue is
- *  empty, return false and the contents of RET_EVENT are unspecified.
- */
-bool al_get_next_event(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_EVENT *ret_event)
-{
-   ASSERT(queue);
-   ASSERT(ret_event);
-
-   return get_peek_or_drop_next_event(queue, ret_event, true);
-}
-
-
-
 /* Function: al_peek_next_event
  *  Copy the contents of the next event packet in the event queue
  *  specified into RET_EVENT and return true.  The original event
@@ -268,10 +256,21 @@ bool al_get_next_event(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_EVENT *ret_event)
  */
 bool al_peek_next_event(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_EVENT *ret_event)
 {
+   ALLEGRO_EVENT *next_event;
    ASSERT(queue);
    ASSERT(ret_event);
 
-   return get_peek_or_drop_next_event(queue, ret_event, false);
+   _al_mutex_lock(&queue->mutex);
+
+   next_event = get_next_event_if_any(queue, false);
+   if (next_event) {
+      copy_event(ret_event, next_event);
+      ref_if_user_event(ret_event);
+   }
+
+   _al_mutex_unlock(&queue->mutex);
+
+   return (next_event ? true : false);
 }
 
 
@@ -282,9 +281,17 @@ bool al_peek_next_event(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_EVENT *ret_event)
  */
 void al_drop_next_event(ALLEGRO_EVENT_QUEUE *queue)
 {
+   ALLEGRO_EVENT *next_event;
    ASSERT(queue);
 
-   get_peek_or_drop_next_event(queue, NULL, true);
+   _al_mutex_lock(&queue->mutex);
+
+   next_event = get_next_event_if_any(queue, true);
+   if (next_event) {
+      unref_if_user_event(next_event);
+   }
+
+   _al_mutex_unlock(&queue->mutex);
 }
 
 
@@ -294,9 +301,19 @@ void al_drop_next_event(ALLEGRO_EVENT_QUEUE *queue)
  */
 void al_flush_event_queue(ALLEGRO_EVENT_QUEUE *queue)
 {
+   unsigned int i;
    ASSERT(queue);
 
    _al_mutex_lock(&queue->mutex);
+
+   /* Decrement reference counts on all user events. */
+   i = queue->events_tail;
+   while (i != queue->events_head) {
+      ALLEGRO_EVENT *old_ev = _al_vector_ref(&queue->events, i);
+      unref_if_user_event(old_ev);
+      i = circ_array_next(&queue->events, i);
+   }
+
    queue->events_head = queue->events_tail = 0;
    _al_mutex_unlock(&queue->mutex);
 }
@@ -471,6 +488,33 @@ static void copy_event(ALLEGRO_EVENT *dest, const ALLEGRO_EVENT *src)
 
 
 
+/* Increment a user event's reference count, if the event passed is a user
+ * event and requires it.
+ */
+static void ref_if_user_event(ALLEGRO_EVENT *event)
+{
+   if (ALLEGRO_EVENT_TYPE_IS_USER(event->type)) {
+      ALLEGRO_USER_EVENT_DESCRIPTOR *descr = event->user.__internal__descr;
+      if (descr) {
+         __sync_fetch_and_add(&descr->refcount, 1);
+      }
+   }
+}
+
+
+
+/* Decrement a user event's reference count, if the event passed is a user
+ * event and requires it.
+ */
+static void unref_if_user_event(ALLEGRO_EVENT *event)
+{
+   if (ALLEGRO_EVENT_TYPE_IS_USER(event->type)) {
+      al_unref_user_event(&event->user);
+   }
+}
+
+
+
 /* Internal function: _al_event_queue_push_event
  *  Event sources call this function when they have something to add to
  *  the queue.  If a queue cannot accept the event, the event's
@@ -490,6 +534,7 @@ void _al_event_queue_push_event(ALLEGRO_EVENT_QUEUE *queue,
    {
       new_event = alloc_event(queue);
       copy_event(new_event, orig_event);
+      ref_if_user_event(new_event);
 
       /* Wake up threads that are waiting for an event to be placed in
        * the queue.
@@ -564,6 +609,9 @@ static void discard_events_of_source(ALLEGRO_EVENT_QUEUE *queue,
          new_event = _al_vector_alloc_back(&queue->events);
          copy_event(new_event, old_event);
       }
+      else {
+         unref_if_user_event(old_event);
+      }
       i = circ_array_next(&old_events, i);
    }
 
@@ -582,6 +630,32 @@ static void discard_events_of_source(ALLEGRO_EVENT_QUEUE *queue,
 
 
 
+/* Function: al_unref_user_event
+ *  Unreference a user-defined event. This must be called on any user event
+ *  that you get from <al_get_next_event>, <al_peek_next_event>,
+ *  <al_wait_for_event>, etc. which is reference counted.
+ *  This function does nothing if the event is not reference counted.
+ *
+ *  See also: <al_emit_user_event>.
+ */
+void al_unref_user_event(ALLEGRO_USER_EVENT *event)
+{
+   ALLEGRO_USER_EVENT_DESCRIPTOR *descr;
+   ASSERT(event);
+
+   descr = event->__internal__descr;
+   if (descr) {
+      ASSERT(descr->refcount > 0);
+      /* XXX uses gcc extension */
+      if (__sync_sub_and_fetch(&descr->refcount, 1) == 0) {
+         (descr->dtor)(event);
+         _AL_FREE(descr);
+      }
+   }
+}
+
+
+
 /*
  * Local Variables:
  * c-basic-offset: 3
diff --git a/src/evtsrc.c b/src/evtsrc.c
index 691c2ef..4ba0c8f 100644
--- a/src/evtsrc.c
+++ b/src/evtsrc.c
@@ -202,27 +202,36 @@ void al_destroy_user_event_source(ALLEGRO_EVENT_SOURCE *src)
  *  Returns `false' if the event source isn't registered with any queues,
  *  hence the event wouldn't have been delivered into any queues.
  *
- *  It is not recommended to have user events that hold unique references to
- *  dynamically allocated memory blocks.  If you choose to do that, it is your
- *  responsibility to manage that memory.  Event structures are *copied* into
- *  event queues.  If a user event is emitted by an event source registered
- *  with multiple queues, there would be multiple events pointing to the same
- *  piece of dynamically allocated memory.  In some cases, such as when an
- *  event queue is destroyed, when an event source is removed from a queue, or
- *  if you call <al_flush_event_queue>, events may be dropped without further
- *  warning.  If one of those events held a unique pointer to a memory block
- *  then that would result in a memory leak.
+ *  Reference counting will be performed on the event if `dtor' is non-NULL.
+ *  When the reference count drops to zero `dtor' will be called with a copy of
+ *  the event as an argument.  It should free the resources associated with
+ *  the event.  If `dtor' is NULL then reference counting will not be
+ *  performed.
+ *
+ *  You need to call <al_unref_user_event> when you are done with a reference
+ *  counted user event that you have gotten from <al_get_next_event>,
+ *  <al_peek_next_event>, <al_wait_for_event>, etc.  You may, but do not need
+ *  to, call <al_unref_user_event> on non-reference counted user events.
  */
 bool al_emit_user_event(ALLEGRO_EVENT_SOURCE *src,
-   ALLEGRO_EVENT *event)
+   ALLEGRO_EVENT *event, void (*dtor)(ALLEGRO_USER_EVENT *))
 {
    size_t num_queues;
    bool rc;
 
    ASSERT(src);
    ASSERT(event);
-   /* We could check if it's not a builtin event type. */
-   ASSERT(event->type != 0);
+   ASSERT(ALLEGRO_EVENT_TYPE_IS_USER(event->any.type));
+
+   if (dtor) {
+      ALLEGRO_USER_EVENT_DESCRIPTOR *descr = _AL_MALLOC(sizeof(*descr));
+      descr->refcount = 0;
+      descr->dtor = dtor;
+      event->user.__internal__descr = descr;
+   }
+   else {
+      event->user.__internal__descr = NULL;
+   }
 
    _al_event_source_lock(src);
    {
@@ -238,6 +247,11 @@ bool al_emit_user_event(ALLEGRO_EVENT_SOURCE *src,
    }
    _al_event_source_unlock(src);
 
+   if (dtor && !rc) {
+      dtor(&event->user);
+      _AL_FREE(event->user.__internal__descr);
+   }
+
    return rc;
 }
 


Mail converted by MHonArc 2.6.19+ http://listengine.tuxfamily.org/