[libpeas] Make PeasEngine thread-safe



commit 47ff5e97b4066822b773c04a063914fecafb20bf
Author: Garrett Regier <garrett regier riftio com>
Date:   Sat Nov 8 12:24:09 2014 -0800

    Make PeasEngine thread-safe
    
    https://bugzilla.gnome.org/show_bug.cgi?id=739619

 libpeas/peas-engine.c                     |  276 +++++++++++++++++++++--------
 libpeas/peas-utils.c                      |   19 ++-
 libpeas/peas-utils.h                      |    3 +
 tests/libpeas/extension-py.c              |    3 +-
 tests/libpeas/testing/testing-extension.c |    9 +-
 5 files changed, 228 insertions(+), 82 deletions(-)
---
diff --git a/libpeas/peas-engine.c b/libpeas/peas-engine.c
index 92cf055..520d8ee 100644
--- a/libpeas/peas-engine.c
+++ b/libpeas/peas-engine.c
@@ -79,7 +79,6 @@ static GParamSpec *properties[N_PROPERTIES] = { NULL };
 
 typedef struct _LoaderInfo {
   PeasPluginLoader *loader;
-  PeasObjectModule *module;
 
   guint enabled : 1;
   guint failed : 1;
@@ -91,15 +90,18 @@ typedef struct _SearchPath {
 } SearchPath;
 
 struct _PeasEnginePrivate {
-  GList *search_paths;
+  LoaderInfo loaders[PEAS_UTILS_N_LOADERS];
 
+  GList *search_paths;
   GList *plugin_list;
 
   guint in_dispose : 1;
 };
 
-static PeasEngine *default_engine = NULL;
 static gboolean shutdown = FALSE;
+static PeasEngine *default_engine = NULL;
+
+static GMutex loaders_lock;
 static LoaderInfo loaders[PEAS_UTILS_N_LOADERS];
 
 static void peas_engine_load_plugin_real   (PeasEngine     *engine,
@@ -294,17 +296,29 @@ peas_engine_prepend_search_path (PeasEngine  *engine,
 }
 
 static void
+default_engine_weak_notify (gpointer    unused,
+                            PeasEngine *engine)
+{
+  g_warn_if_fail (g_atomic_pointer_compare_and_exchange (&default_engine,
+                                                         engine, NULL));
+}
+
+static void
 peas_engine_init (PeasEngine *engine)
 {
   /* Set the default engine here and not in constructor() to make sure
    * that if a plugin is loaded and calls peas_engine_get_default()
    * that this engine is returned and not another.
+   *
+   * While peas_engine_get_default() is not thread-safe
+   * we are using atomics here to try and prevent bad
+   * things from happening.
    */
-  if (default_engine == NULL)
+  if (g_atomic_pointer_compare_and_exchange (&default_engine, NULL, engine))
     {
-      default_engine = engine;
-      g_object_add_weak_pointer (G_OBJECT (engine),
-                                 (gpointer *) &default_engine);
+      g_object_weak_ref (G_OBJECT (engine),
+                         (GWeakNotify) default_engine_weak_notify,
+                         NULL);
     }
 
   engine->priv = G_TYPE_INSTANCE_GET_PRIVATE (engine,
@@ -312,7 +326,11 @@ peas_engine_init (PeasEngine *engine)
                                               PeasEnginePrivate);
 
   engine->priv->in_dispose = FALSE;
+
+  /* The C plugin loader is always enabled */
+  engine->priv->loaders[peas_utils_get_loader_id ("C")].enabled = TRUE;
 }
+
 /**
  * peas_engine_garbage_collect:
  * @engine: A #PeasEngine.
@@ -331,8 +349,10 @@ peas_engine_garbage_collect (PeasEngine *engine)
 
   for (i = 0; i < G_N_ELEMENTS (loaders); ++i)
     {
-      if (loaders[i].loader != NULL)
-        peas_plugin_loader_garbage_collect (loaders[i].loader);
+      LoaderInfo *loader_info = &engine->priv->loaders[i];
+
+      if (loader_info->loader != NULL)
+        peas_plugin_loader_garbage_collect (loader_info->loader);
     }
 }
 
@@ -348,6 +368,9 @@ peas_engine_constructor (GType                  type,
                "plugins engine as modules are not supported.");
     }
 
+  /* Don't need to use atomics as peas_engine_shutdown()
+   * is private API and as such is not multithread-safe
+   */
   if (shutdown)
     {
       g_error ("libpeas cannot create a plugin engine "
@@ -408,7 +431,9 @@ peas_engine_dispose (GObject *object)
 {
   PeasEngine *engine = PEAS_ENGINE (object);
   GList *item;
+  gint i;
 
+  /* See peas_engine_unload_plugin_real() */
   engine->priv->in_dispose = TRUE;
 
   /* First unload all the plugins */
@@ -420,6 +445,14 @@ peas_engine_dispose (GObject *object)
         peas_engine_unload_plugin (engine, info);
     }
 
+  /* Then destroy the plugin loaders */
+  for (i = 0; i < G_N_ELEMENTS (engine->priv->loaders); ++i)
+    {
+      LoaderInfo *loader_info = &engine->priv->loaders[i];
+
+      g_clear_object (&loader_info->loader);
+    }
+
   G_OBJECT_CLASS (peas_engine_parent_class)->dispose (object);
 }
 
@@ -577,41 +610,19 @@ load_module (const gchar *module_name,
   module = peas_object_module_new (module_name, module_dir, TRUE);
 
   if (!g_type_module_use (G_TYPE_MODULE (module)))
-    {
-      g_object_unref (module);
-      return NULL;
-    }
+    g_clear_object (&module);
 
   return module;
 }
 
-static PeasPluginLoader *
-get_plugin_loader (PeasEngine *engine,
-                   gint        loader_id)
+static PeasObjectModule *
+get_plugin_loader_module (gint loader_id)
 {
   gint i, j;
-  LoaderInfo *loader_info;
+  PeasObjectModule *module;
   const gchar *loader_name;
   gchar *module_name, *module_dir;
 
-  loader_info = &loaders[loader_id];
-
-  if (!loader_info->enabled)
-    {
-      g_warning ("The '%s' plugin loader has not been enabled",
-                 peas_utils_get_loader_from_id (loader_id));
-      return NULL;
-    }
-
-  if (loader_info->loader != NULL || loader_info->failed)
-    return loader_info->loader;
-
-  if (peas_utils_get_loader_id ("C") == loader_id)
-    {
-      loader_info->loader = peas_plugin_loader_c_new ();
-      return loader_info->loader;
-    }
-
   loader_name = peas_utils_get_loader_from_id (loader_id);
   module_name = g_strconcat (loader_name, "loader", NULL);
   module_dir = peas_dirs_get_plugin_loaders_dir ();
@@ -625,14 +636,14 @@ get_plugin_loader (PeasEngine *engine,
 
   module_name[j] = '\0';
 
-  loader_info->module = load_module (module_name, module_dir);
+  module = load_module (module_name, module_dir);
 
-  if (loader_info->module == NULL)
+  if (module == NULL)
     {
       gchar *tmp = module_dir;
 
       module_dir = g_build_filename (module_dir, loader_name, NULL);
-      loader_info->module = load_module (module_name, module_dir);
+      module = load_module (module_name, module_dir);
 
       g_free (tmp);
     }
@@ -640,33 +651,110 @@ get_plugin_loader (PeasEngine *engine,
   g_free (module_dir);
   g_free (module_name);
 
-  if (loader_info->module == NULL)
-    {
-      g_warning ("Could not load plugin loader '%s'", loader_name);
-      loader_info->failed = TRUE;
-      return NULL;
-    }
+  if (module == NULL)
+    g_warning ("Could not load plugin loader '%s'", loader_name);
 
-  loader_info->loader = PEAS_PLUGIN_LOADER (
-        peas_object_module_create_object (loader_info->module,
+  return module;
+}
+
+static PeasPluginLoader *
+create_plugin_loader (gint loader_id)
+{
+  PeasObjectModule *module;
+  PeasPluginLoader *loader;
+
+  module = get_plugin_loader_module (loader_id);
+  if (module == NULL)
+    return NULL;
+
+  loader = PEAS_PLUGIN_LOADER (
+        peas_object_module_create_object (module,
                                           PEAS_TYPE_PLUGIN_LOADER,
                                           0, NULL));
 
-  /* Don't bother unloading the loader's
-   * GTypeModule as it is always resident
+  if (loader == NULL || !peas_plugin_loader_initialize (loader))
+    {
+      g_warning ("Loader '%s' is not a valid PeasPluginLoader instance",
+                 peas_utils_get_loader_from_id (loader_id));
+      g_clear_object (&loader);
+    }
+
+  /* Don't bother unloading the
+   * module as it is always resident
    */
+  return loader;
+}
+
+static PeasPluginLoader *
+get_local_plugin_loader (gint loader_id)
+{
+  LoaderInfo *global_loader_info = &loaders[loader_id];
+
+  if (global_loader_info->failed)
+    return NULL;
 
-  if (loader_info->loader == NULL ||
-      !peas_plugin_loader_initialize (loader_info->loader))
+  if (global_loader_info->loader != NULL)
+    return g_object_ref (global_loader_info->loader);
+
+  if (peas_utils_get_loader_id ("C") == loader_id)
     {
-      g_warning ("Loader '%s' is not a valid PeasPluginLoader instance",
-                 loader_name);
+      global_loader_info->loader = peas_plugin_loader_c_new ();
+      return g_object_ref (global_loader_info->loader);
+    }
 
-      loader_info->failed = TRUE;
-      g_clear_object (&loader_info->loader);
+  global_loader_info->loader = create_plugin_loader (loader_id);
+
+  if (global_loader_info->loader == NULL)
+    {
+      global_loader_info->failed = TRUE;
       return NULL;
     }
 
+  return g_object_ref (global_loader_info->loader);
+}
+
+static PeasPluginLoader *
+get_plugin_loader (PeasEngine *engine,
+                   gint        loader_id)
+{
+  LoaderInfo *loader_info = &engine->priv->loaders[loader_id];
+  LoaderInfo *global_loader_info = &loaders[loader_id];
+
+  if (loader_info->loader != NULL || loader_info->failed)
+    return loader_info->loader;
+
+  g_mutex_lock (&loaders_lock);
+
+  if (!loader_info->enabled)
+    {
+      if (!global_loader_info->enabled)
+        {
+          g_warning ("The '%s' plugin loader has not been enabled",
+                     peas_utils_get_loader_from_id (loader_id));
+
+          g_mutex_unlock (&loaders_lock);
+          return NULL;
+        }
+
+      g_warning ("The '%s' plugin loader was not enabled "
+                 "for this engine. This will no longer be "
+                 "supported at some point in the future!",
+                 peas_utils_get_loader_from_id (loader_id));
+
+      g_mutex_unlock (&loaders_lock);
+
+      /* Avoid bypassing logic in peas_engine_enable_loader() */
+      peas_engine_enable_loader (engine,
+                                 peas_utils_get_loader_from_id (loader_id));
+      return get_plugin_loader (engine, loader_id);
+    }
+
+  loader_info->loader = get_local_plugin_loader (loader_id);
+
+  if (loader_info->loader == NULL)
+    loader_info->failed = TRUE;
+
+  g_mutex_unlock (&loaders_lock);
   return loader_info->loader;
 }
 
@@ -689,18 +777,16 @@ get_plugin_loader (PeasEngine *engine,
  * loader can only be used from a single thread. Should this happen
  * a deadlock will occur.
  *
- * Due to the use of toggle references in the python and python3
- * bindings only one of them should be enabled. Otherwise vast
- * memory leaks are to be expected and as such it is an error to
- * enable more than one of them.
- *
- * Note: plugin loaders are shared across #PeasEngines so enabling
- *       a loader on one #PeasEngine will enable it on all #PeasEngines.
+ * Note: plugin loaders used to be shared across #PeasEngines so enabling
+ *       a loader on one #PeasEngine would enable it on all #PeasEngines.
+ *       This behavior has been kept to avoid breaking applications,
+ *       however a warning has been added to help applications transition.
  **/
 void
 peas_engine_enable_loader (PeasEngine  *engine,
                            const gchar *loader_name)
 {
+  LoaderInfo *loader_info;
   gint loader_id;
 
   g_return_if_fail (PEAS_IS_ENGINE (engine));
@@ -714,32 +800,52 @@ peas_engine_enable_loader (PeasEngine  *engine,
       return;
     }
 
-  if (loaders[loader_id].enabled)
+  loader_info = &engine->priv->loaders[loader_id];
+  if (loader_info->enabled || loader_info->failed)
     return;
 
-  /* The demo and some tests need to load multiple loaders */
-  if (g_getenv ("PEAS_ALLOW_ALL_LOADERS") == NULL)
+  g_mutex_lock (&loaders_lock);
+
+  if (loaders[loader_id].enabled)
+    {
+      loader_info->enabled = TRUE;
+      g_mutex_unlock (&loaders_lock);
+      return;
+    }
+
+  /* Some tests check for mixed versions this way */
+  if (g_getenv ("PEAS_ALLOW_CONFLICTING_LOADERS") == NULL)
     {
       gint i;
-      static const gchar *other_loaders[] = {"python", "python3"};
+      const gint *loader_ids;
 
-      for (i = 0; i < G_N_ELEMENTS (other_loaders); ++i)
+      loader_ids = peas_utils_get_conflicting_loaders_from_id (loader_id);
+
+      /* Some loaders conflict with each other
+       * and cannot be used in the same process
+       */
+      for (i = 0; loader_ids[i] != -1; ++i)
         {
-          if (loaders[peas_utils_get_loader_id (other_loaders[i])].enabled)
-            {
-              g_warning ("Cannot enable plugin loader '%s' as the "
-                         "'%s' plugin loader has already been enabled.",
-                         loader_name, other_loaders[i]);
-              loaders[loader_id].failed = TRUE;
-              return;
-            }
+          if (!loaders[loader_ids[i]].enabled)
+            continue;
+
+          g_warning ("Cannot enable plugin loader '%s' as the "
+                     "'%s' plugin loader is already enabled.", loader_name,
+                     peas_utils_get_loader_from_id (loader_ids[i]));
+
+          loader_info->failed = TRUE;
+          g_mutex_unlock (&loaders_lock);
+          return;
         }
     }
 
   /* We do not load the plugin loader immediately and instead
    * load it in get_plugin_loader() so that it is loaded lazily.
    */
+  loader_info->enabled = TRUE;
   loaders[loader_id].enabled = TRUE;
+
+  g_mutex_unlock (&loaders_lock);
 }
 
 /**
@@ -1271,6 +1377,9 @@ peas_engine_new (void)
  * If no #PeasEngine subclass has been instantiated yet, the first call
  * of this function will return a new instance of #PeasEngine.
  *
+ * Note: this function should never be used when multiple threads are
+ *       using libpeas API as it is not thread-safe.
+ *
  * Returns: (transfer none): the existing instance of #PeasEngine.
  */
 PeasEngine *
@@ -1278,7 +1387,16 @@ peas_engine_get_default (void)
 {
   /* peas_engine_new() will cause the default to be set for us */
   if (default_engine == NULL)
-    return peas_engine_new ();
+    {
+      PeasEngine *engine = peas_engine_new ();
+
+      if (engine != default_engine)
+        {
+          g_warning ("peas_engine_get_default() must not be called when "
+                     "multiple threads are using libpeas API");
+          g_object_unref (engine);
+        }
+    }
 
   return default_engine;
 }
@@ -1288,6 +1406,8 @@ peas_engine_get_default (void)
  *
  * Frees memory shared by PeasEngines.
  * No libpeas API should be called after calling this.
+ *
+ * Note: this is private API and as such is not thread-safe.
  */
 void
 peas_engine_shutdown (void)
@@ -1299,6 +1419,8 @@ peas_engine_shutdown (void)
 
   shutdown = TRUE;
 
+  g_mutex_lock (&loaders_lock);
+
   for (i = 0; i < G_N_ELEMENTS (loaders); ++i)
     {
       LoaderInfo *loader_info = &loaders[i];
@@ -1312,8 +1434,8 @@ peas_engine_shutdown (void)
           g_assert (loader_info->loader == NULL);
         }
 
-      loader_info->module = NULL;
-      loader_info->enabled = FALSE;
       loader_info->failed = TRUE;
     }
+
+  g_mutex_unlock (&loaders_lock);
 }
diff --git a/libpeas/peas-utils.c b/libpeas/peas-utils.c
index a2adb52..c5efb6f 100644
--- a/libpeas/peas-utils.c
+++ b/libpeas/peas-utils.c
@@ -31,7 +31,15 @@
 
 static const gchar *all_plugin_loaders[] = {"c", "lua5.1",
                                             "python", "python3"};
+static const gint conflicting_plugin_loaders[PEAS_UTILS_N_LOADERS][2] = {
+  { -1, -1 }, /* c       => {}          */
+  { -1, -1 }, /* lua5.1  => {}          */
+  {  3, -1 }, /* python  => { python3 } */
+  {  2, -1 }  /* python3 => { python  } */
+};
+
 G_STATIC_ASSERT (G_N_ELEMENTS (all_plugin_loaders) == PEAS_UTILS_N_LOADERS);
+G_STATIC_ASSERT (G_N_ELEMENTS (conflicting_plugin_loaders) == PEAS_UTILS_N_LOADERS);
 
 static void
 add_all_interfaces (GType      iface_type,
@@ -180,8 +188,17 @@ const gchar *
 peas_utils_get_loader_from_id (gint loader_id)
 {
   g_return_val_if_fail (loader_id >= 0, NULL);
-  g_return_val_if_fail (loader_id < G_N_ELEMENTS (all_plugin_loaders), NULL);
+  g_return_val_if_fail (loader_id < PEAS_UTILS_N_LOADERS, NULL);
 
   return all_plugin_loaders[loader_id];
 }
 
+const gint *
+peas_utils_get_conflicting_loaders_from_id (gint loader_id)
+{
+  g_return_val_if_fail (loader_id >= 0, NULL);
+  g_return_val_if_fail (loader_id < PEAS_UTILS_N_LOADERS, NULL);
+
+  return conflicting_plugin_loaders[loader_id];
+}
+
diff --git a/libpeas/peas-utils.h b/libpeas/peas-utils.h
index 024d369..86d1324 100644
--- a/libpeas/peas-utils.h
+++ b/libpeas/peas-utils.h
@@ -35,5 +35,8 @@ gboolean  peas_utils_valist_to_parameter_list (GType         iface_type,
 gint     peas_utils_get_loader_id             (const gchar  *loader) G_GNUC_CONST;
 const gchar *
          peas_utils_get_loader_from_id        (gint          loader_id) G_GNUC_CONST;
+const gint *
+         peas_utils_get_conflicting_loaders_from_id
+                                              (gint          loader_id) G_GNUC_CONST;
 
 #endif /* __PEAS_UTILS_H__ */
diff --git a/tests/libpeas/extension-py.c b/tests/libpeas/extension-py.c
index 6b6a472..b668558 100644
--- a/tests/libpeas/extension-py.c
+++ b/tests/libpeas/extension-py.c
@@ -207,8 +207,7 @@ test_extension_py_mixed_python_subprocess (void)
   testing_util_push_log_hook ("*'" ALT_PY_LOADER_STR
                               "' is not a valid PeasPluginLoader*");
 
-  /* Required when loading multiple loaders */
-  g_setenv ("PEAS_ALLOW_ALL_LOADERS", "1", TRUE);
+  g_setenv ("PEAS_ALLOW_CONFLICTING_LOADERS", "1", TRUE);
 
   engine = testing_engine_new ();
   peas_engine_enable_loader (engine, ALT_PY_LOADER_STR);
diff --git a/tests/libpeas/testing/testing-extension.c b/tests/libpeas/testing/testing-extension.c
index e1f6a59..2fd5dda 100644
--- a/tests/libpeas/testing/testing-extension.c
+++ b/tests/libpeas/testing/testing-extension.c
@@ -46,6 +46,7 @@ struct _TestFixture {
   PeasPluginInfo *info;
 };
 
+static gchar *loader = NULL;
 static gchar *extension_plugin = NULL;
 
 static void
@@ -53,6 +54,7 @@ test_setup (TestFixture    *fixture,
             gconstpointer  data)
 {
   fixture->engine = testing_engine_new ();
+  peas_engine_enable_loader (fixture->engine, loader);
   fixture->info = peas_engine_get_plugin_info (fixture->engine,
                                                extension_plugin);
 }
@@ -386,13 +388,15 @@ test_extension_call_multi_args (PeasEngine     *engine,
   } G_STMT_END
 
 void
-testing_extension_basic (const gchar *loader)
+testing_extension_basic (const gchar *loader_)
 {
   gint i, j;
   gchar *loader_name;
   PeasEngine *engine;
 
-  loader_name = g_new0 (gchar, strlen (loader));
+  loader = g_strdup (loader_);
+
+  loader_name = g_new0 (gchar, strlen (loader) + 1);
   for (i = 0, j = 0; loader[i] != '\0'; ++i)
     {
       if (loader[i] != '.')
@@ -451,6 +455,7 @@ testing_extension_run_tests (void)
   retval = testing_run_tests ();
 
   g_free (extension_plugin);
+  g_free (loader);
 
   return retval;
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]