Branch: refs/heads/master Author: Colomban Wendling ban@herbesfolles.org Committer: Colomban Wendling ban@herbesfolles.org Date: Tue, 06 Oct 2015 13:53:14 UTC Commit: c6952c75999c3775a5d7342bc3892c5c9451fb39 https://github.com/geany/geany/commit/c6952c75999c3775a5d7342bc3892c5c9451fb...
Log Message: ----------- Merge pull request #629 from kugel-/pluxy
Add support for plugins acting as proxies for foreign plugins, promoting foreign plugins to first-class citizen.
Modified Paths: -------------- doc/plugins.dox plugins/Makefile.am plugins/demoproxy.c plugins/demoproxytest.px po/POTFILES.skip src/plugindata.h src/pluginprivate.h src/plugins.c src/pluginutils.c src/utils.h
Modified: doc/plugins.dox 397 lines changed, 397 insertions(+), 0 deletions(-) =================================================================== @@ -43,6 +43,7 @@ GeanyFuncs::cleanup functions).
@section pluginsupport Plugin Support - @link howto Plugin HowTo @endlink - get started +- @ref proxy - @ref legacy - @link plugindata.h Plugin Datatypes and Macros @endlink - @link pluginsignals.c Plugin Signals @endlink @@ -725,4 +726,400 @@ void geany_load_module(GeanyPlugin *plugin) @endcode
+@page proxy Proxy Plugin HowTo + +@section proxy_intro Introduction + +Geany has built-in support for plugins. These plugins can alter the way Geany operates in many +imaginable ways which leaves little to be desired. + +However, there is one significant short-coming. Due to the infrastructure, Geany's built-in support +only covers plugins written in C, perhaps C++ and Vala. Basically all languages which can be +compiled into native shared libraries and can link GTK libraries. This excludes dynamic languages +such as Python. + +Geany provides a mechanism to enable support for those languages. Native plugins can register as +proxy plugins by being a normal plugin to the Geany-side and by providing a bridge to write plugins +in another language on the other side. + +These plugins are also called sub-plugins. This refers to the relation to their proxy. +To Geany they are first-class citizens. + +@section proxy_protocol Writing a Proxy Plugin + +The basic idea is that a proxy plugin provides methods to match, load and unload one or more +sub-plugin plugins in an abstract manner: + + - Matching consists of providing a list of supported file extensions for the sub-plugins and + a mechanism to resolve file extension uncertainty or ambiguity. The matching makes the plugin + visible to the user within the Plugin Manager. + - Loading consists of loading the sub-plugin's file, passing the file to some form of interpreter + and calling GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() on behalf of the sub-plugin + at some point. + - Unloading simply reverses the effect of loading. + +For providing these methods, GeanyPlugin has a field GeanyProxyFuncs which contains three function +pointers which must be initialized proir to calling geany_plugin_register_proxy(). This should be +done in the GeanyPluginFuncs::init function of the proxy plugin. + + - In the call to geany_plugin_register_proxy() the proxy plugin passes a list of file extensions. + When Geany scans through its plugin directories as usual it will also look for files with + that extensions and consider found files as plugin candidate. + - GeanyProxyFuncs::probe may be implemented to probe if a plugin candidate (that has one of the + provided file extensions) is actually a plugin. This may depend on the plugin file itself in + case of ambiguity or availability of runtime dependencies or even configuration. + @ref PROXY_IGNORED or @ref PROXY_MATCHED should be returned, possibly in combination + with the @ref PROXY_NOLOAD flag. Not implementing GeanyProxyFuncs::probe at all is eqivalent to + always returning @ref PROXY_MATCHED. + - GeanyProxyFuncs::load must be implemented to actually load the plugin. It is called by Geany + when the user enables the sub-plugin. What "loading" means is entirely up to the proxy plugin and + probably depends on the interpreter of the dynamic language that shall be supported. After + setting everything up as necessary GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() must + be called to register the sub-plugin. + - GeanyProxyFuncs::unload must be implemented and is called when the user unchecks the sub-plugin + or when Geany exits. Here, the proxy should release any references or memory associated to the + sub-plugin. Note that if GeanyProxyFuncs::load didn't succeed, i.e. didn't successfully register + the sub-plugin, then this function won't be called. + +GeanyProxyFuncs::load and GeanyProxyFuncs::unload receive two GeanyPlugin pointers: One that +corresponds to the proxy itself and another that corresponds to the sub-plugin. The sub-plugin's +one may be used to call various API functions on behalf of the sub-plugin, including +GEANY_PLUGIN_REGISTER() and GEANY_PLUGIN_REGISTER_FULL(). + +GeanyProxyFuncs::load may return a pointer that is passed back to GeanyProxyFuncs::unload. This can +be used to store proxy-defined but sub-plugin-specific data required for unloading. However, this +pointer is not passed to the sub-plugin's GeanyPluginFuncs. To arrange for that, you want to call +GEANY_PLUGIN_REGISTER_FULL(). This method is the key to enable proxy plugins to wrap the +GeanyPluginFuncs of all sub-plugins and yet multiplex between multiple sub-plugin, for example by +storing a per-sub-plugin interpreter context. + +@note If the pointer returned from GeanyProxyFuncs::load is the same that is passed to +GEANY_PLUGIN_REGISTER_FULL() then you must pass NULL as free_func, because that would be invoked +prior to unloading. Insert the corresponding code into GeanyProxyFuncs::unload. + +@section proxy_compat_guideline Guideline for Checking Compatiblity + +Determining if a plugin candidate is compatible is not a single test. There are multiple levels and +each should be handled differently in order to give the user a consistent feedback. + +Consider the 5 basic cases: + +1) A candidate comes with a suitable file extension but is not a workable plugin file at all. For +example, your proxy supports plugins written in a shell script (.sh) but the shebang of that script +points to an incompatible shell (or even lacks a shebang). You should check for this in +GeanyProxyFuncs::probe() and return @ref PROXY_IGNORED which hides that script from the Plugin +Manager and allows other enabled proxy plugins to pick it up. GeanyProxyFuncs::probe() returning +@ref PROXY_IGNORED is an indication that the candidate is meant for another proxy, or the user +placed the file by accident in one of Geany's plugin directories. In other words the candidate +simply doesn't correspond to your proxy. Thus any noise by debug messages for this case is +undesirable. + +2) A proxy plugin provides its own, versioned API to sub-plugin. The API version of the sub-plugin +is not compatible with the API exposed by the proxy. GeanyProxyFuncs::probe() should never perform +a version check because its sole purpose is to indicate a proxy's correspondence to a given +candidate. It should return @ref PROXY_MATCHED instead. Later, Geany will invoke the +GeanyProxyFuncs::load(), and this function is the right place for a version check. If it fails then +you simply do not call GEANY_PLUGIN_REGISTER(), but rather print a debug message. The result is +that the sub-plugin is not shown in the Plugin Manager at all. This is consistent with the +treatment of native plugins by Geany. + +3) The sub-plugin is also depending on Geany's API version (whether it is or not depends on the +design of the proxy). In this case do not do anything special but forward the API version the +sub-plugin is written/compiled against to GEANY_PLUGIN_REGISTER(). Here, Geany will perform its own +compatiblity check, allowing for a consistent user feedback. The result is again that the +sub-plugin is hidden from the Plugin Manager, like in case 2. But Geany will print a debug message +so you can skip that. + + +If you have even more cases try to fit it into case 1 or 2, depending on whether other proxy +plugins should get a chance to load the candidate or not. + +@section proxy_dep_guideline Guideline for Runtime Errors + +A sub-plugin might not be able to run even if it's perfectly compatible with its proxy. This +includes the case when it lacks certain runtime dependencies such as programs or modules but also +syntactic problems or other errors. + +There are two basic classes: + +1) Runtime errors that can be determined at load time. For example, the shebang of a script +indicates a specific interpeter version but that version is not installed on the system. Your proxy +should respond the same way as for version-incompatible plugins: don't register the plugin at +all, but leave a message the user suggesting what has to be installed in order to work. Handle +syntax errors in the scripts of sub-plugins the same way if possible. + +2) Runtime errors that cannot be determined without actually running the plugin. An example would +be missing modules in Python scripts. If your proxy has no way of foreseeing the problem the plugin +will be registered normally. However, you can catch runtime errors by implementing +GeanyPluginFuncs::init() on the plugin's behalf. This is called after user activation and allows to +indicate errors by returning @c FALSE. However, allowing the user to enable a plugin and then +disabling anyway is a poor user experience. + +Therefore, if possible, try to fail fast and disallow registration. + +@section Proxy Plugin Example + +In this section a dumb example proxy plugin is shown in order to give a practical starting point. +The sub-plugin are not actually code but rather a ini-style description of one or more menu items +that are added to Geany's tools menu and a help dialog. Real world sub-plugins would contain actual +code, usually written in a scripting language. + +A sub-plugin file looks like this: + +@code{.ini} +#!!PROXY_MAGIC!! + +[Init] +item0 = Bam +item1 = Foo +item2 = Bar + +[Help] +text = I'm a simple test. Nothing to see! + +[Info] +name = Demo Proxy Tester +description = I'm a simple test. Nothing to see! +version = 0.1 +author = The Geany developer team +@endcode + +The first line acts as a verification that this file is truly a sub-plugin. Within the [Init] section +there is the menu items for Geany's tools menu. The [Help] section declares the sub-plugins help +text which is shown in its help dialog (via GeanyPluginFuncs::help). The [Info] section is +used as-is for filling the sub-plugins PluginInfo fields. + +That's it, this dumb format is purely declarative and contains no logic. Yet we will create plugins +from it. + +We start by registering the proxy plugin to Geany. There is nothing special to it compared to +normal plugins. A proxy plugin must also fill its own @ref PluginInfo and @ref GeanyPluginFuncs, +followed by registering through GEANY_PLUGIN_REGISTER(). + + +@code{.c} + +/* Called by Geany to initialize the plugin. */ +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + // ... +} + + +/* Called by Geany before unloading the plugin. */ +static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data) +{ + // ... +} + + +G_MODULE_EXPORT +void geany_load_module(GeanyPlugin *plugin) +{ + plugin->info->name = _("Demo Proxy"); + plugin->info->description = _("Example Proxy."); + plugin->info->version = "0.1"; + plugin->info->author = _("The Geany developer team"); + + plugin->funcs->init = demoproxy_init; + plugin->funcs->cleanup = demoproxy_cleanup; + + GEANY_PLUGIN_REGISTER(plugin, 225); +} + +@endcode + +The next step is to actually register as a proxy plugin. This is done in demoproxy_init(). +As previously mentioned, it needs a list of accepted file extensions and a set of callback +functions. + +@code{.c} +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + const gchar *extensions[] = { "ini", "px", NULL }; + + plugin->proxy_funcs->probe = demoproxy_probe; + plugin->proxy_funcs->load = demoproxy_load; + plugin->proxy_funcs->unload = demoproxy_unload; + + return geany_plugin_register_proxy(plugin, extensions); +} + +@endcode + +The callback functions deserve a closer look. + +As already mentioned the file format includes a magic first line which must be present. +GeanyProxyFuncs::probe() verifies that it's present and avoids showing the sub-plugin in the +Plugin Manager if not. + +@code{.c} +static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata) +{ + /* We know the extension is right (Geany checks that). For demo purposes we perform an + * additional check. This is not necessary when the extension is unique enough. */ + gboolean match = FALSE; + gchar linebuf[128]; + FILE *f = fopen(filename, "r"); + if (f != NULL) + { + if (fgets(linebuf, sizeof(linebuf), f) != NULL) + match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n"); + fclose(f); + } + return match ? PROXY_MATCHED : PROXY_IGNORED; +} +@endcode + +GeanyProxyFuncs::load is a bit more complex. It reads the file, fills the sub-plugin's PluginInfo +fields and calls GEANY_PLUGIN_REGISTER_FULL(). Additionally, it creates a per-plugin context that +holds GKeyFile instance (a poor man's interpeter context). You can also see that it does not call +GEANY_PLUGIN_REGISTER_FULL() if g_key_file_load_from_file() found an error (probably a syntax +problem) which means the sub-plugin cannot be enabled. + +It also installs wrapper functions for the sub-plugin's GeanyPluginFuncs as ini files aren't code. +It's very likely that your proxy needs something similar because you can only install function +pointers to native code. + +@code{.c} +typedef struct { + GKeyFile *file; + gchar *help_text; + GSList *menu_items; +} +PluginContext; + + +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata); +static void proxy_help(GeanyPlugin *plugin, gpointer pdata); +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata); + + +static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin, + const gchar *filename, gpointer pdata) +{ + GKeyFile *file; + gboolean result; + + file = g_key_file_new(); + result = g_key_file_load_from_file(file, filename, 0, NULL); + + if (result) + { + PluginContext *data = g_new0(PluginContext, 1); + data->file = file; + + plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL); + plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL); + plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL); + plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL); + + plugin->funcs->init = proxy_init; + plugin->funcs->help = proxy_help; + plugin->funcs->cleanup = proxy_cleanup; + + /* Cannot pass g_free as free_func be Geany calls it before unloading, and since + * demoproxy_unload() accesses the data this would be catastrophic */ + GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL); + return data; + } + + g_key_file_free(file); + return NULL; +} +@endcode + +demoproxy_unload() simply releases all resources aquired in demoproxy_load(). It does not have to +do anything else in for unloading. + +@code{.c} +static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata) +{ + PluginContext *data = load_data; + + g_free((gchar *)plugin->info->name); + g_free((gchar *)plugin->info->description); + g_free((gchar *)plugin->info->version); + g_free((gchar *)plugin->info->author); + + g_key_file_free(data->file); + g_free(data); +} +@endcode + +Finally the demo_proxy's wrapper GeanyPluginFuncs. They are called for each possible sub-plugin and +therefore have to multiplex between each using the plugin-defined data pointer. Each is called by +Geany as if it were an ordinary, native plugin. + +proxy_init() actually reads the sub-plugin's file using GKeyFile APIs. It prepares for the help +dialog and installs the menu items. proxy_help() is called when the user clicks the help button in +the Plugin Manager. Consequently, this fires up a suitable dialog, although with a dummy message. +proxy_cleanup() frees all memory allocated in proxy_init(). + +@code{.c} +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + gint i = 0; + gchar *text; + + data = (PluginContext *) pdata; + + /* Normally, you would instruct the VM/interpreter to call into the actual plugin. The + * plugin would be identified by pdata. Because there is no interpreter for + * .ini files we do it inline, as this is just a demo */ + data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL); + while (TRUE) + { + GtkWidget *item; + gchar *key = g_strdup_printf("item%d", i++); + text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL); + g_free(key); + + if (!text) + break; + + item = gtk_menu_item_new_with_label(text); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item); + gtk_widget_set_sensitive(item, FALSE); + data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item); + g_free(text); + } + + return TRUE; +} + + +static void proxy_help(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + GtkWidget *dialog; + + data = (PluginContext *) pdata; + + dialog = gtk_message_dialog_new( + GTK_WINDOW(plugin->geany_data->main_widgets->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", data->help_text); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + _("(From the %s plugin)"), plugin->info->name); + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + + +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data = (PluginContext *) pdata; + + g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy); + g_free(data->help_text); +} +@endcode + + */
Modified: plugins/Makefile.am 10 lines changed, 8 insertions(+), 2 deletions(-) =================================================================== @@ -1,7 +1,8 @@ # Adapted from Pidgin's plugins/Makefile.am, thanks
EXTRA_DIST = \ - makefile.win32 + makefile.win32 \ + demoproxytest.px
plugindir = $(libdir)/geany
@@ -11,6 +12,7 @@ plugins_include_HEADERS = \ geanyplugin.h
demoplugin_la_LDFLAGS = -module -avoid-version -no-undefined +demoproxy_la_LDFLAGS = -module -avoid-version -no-undefined classbuilder_la_LDFLAGS = -module -avoid-version -no-undefined htmlchars_la_LDFLAGS = -module -avoid-version -no-undefined export_la_LDFLAGS = -module -avoid-version -no-undefined @@ -30,9 +32,11 @@ plugin_LTLIBRARIES = \
# Plugins not to be installed noinst_LTLIBRARIES = \ - demoplugin.la + demoplugin.la \ + demoproxy.la
demoplugin_la_SOURCES = demoplugin.c +demoproxy_la_SOURCES = demoproxy.c classbuilder_la_SOURCES = classbuilder.c htmlchars_la_SOURCES = htmlchars.c export_la_SOURCES = export.c @@ -41,6 +45,7 @@ filebrowser_la_SOURCES = filebrowser.c splitwindow_la_SOURCES = splitwindow.c
demoplugin_la_CFLAGS = -DG_LOG_DOMAIN=""Demoplugin"" -DLOCALEDIR=""$(LOCALEDIR)"" +demoproxy_la_CFLAGS = -DG_LOG_DOMAIN=""Demoproxy"" classbuilder_la_CFLAGS = -DG_LOG_DOMAIN=""Classbuilder"" htmlchars_la_CFLAGS = -DG_LOG_DOMAIN=""HTMLChars"" export_la_CFLAGS = -DG_LOG_DOMAIN=""Export"" @@ -49,6 +54,7 @@ filebrowser_la_CFLAGS = -DG_LOG_DOMAIN=""FileBrowser"" splitwindow_la_CFLAGS = -DG_LOG_DOMAIN=""SplitWindow""
demoplugin_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) +demoproxy_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) classbuilder_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) htmlchars_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) export_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) -lm
Modified: plugins/demoproxy.c 202 lines changed, 202 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,202 @@ +/* + * demoproxy.c - this file is part of Geany, a fast and lightweight IDE + * + * Copyright 2015 Thomas Martitz <kugel(at)rockbox(dot)org> + * + * This program 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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. + */ + +/** + * Demo proxy - example of a basic proxy plugin for Geany. Sub-plugins add menu items to the + * Tools menu and have a help dialog. + * + * Note: This is compiled but not installed by default. On Unix, you can install it by compiling + * Geany and then copying (or symlinking) to the plugins/demoproxy.so and + * plugins/demoproxytest.px files to ~/.config/geany/plugins + * - it will be loaded at next startup. + */ + +/* plugin API, always comes first */ +#include "geanyplugin.h" + +typedef struct { + GKeyFile *file; + gchar *help_text; + GSList *menu_items; +} +PluginContext; + + +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + gint i = 0; + gchar *text; + + data = (PluginContext *) pdata; + + /* Normally, you would instruct the VM/interpreter to call into the actual plugin. The + * plugin would be identified by pdata. Because there is no interpreter for + * .ini files we do it inline, as this is just a demo */ + data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL); + while (TRUE) + { + GtkWidget *item; + gchar *key = g_strdup_printf("item%d", i++); + text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL); + g_free(key); + + if (!text) + break; + + item = gtk_menu_item_new_with_label(text); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item); + gtk_widget_set_sensitive(item, FALSE); + data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item); + g_free(text); + } + + return TRUE; +} + + +static void proxy_help(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + GtkWidget *dialog; + + data = (PluginContext *) pdata; + + dialog = gtk_message_dialog_new( + GTK_WINDOW(plugin->geany_data->main_widgets->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", data->help_text); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + _("(From the %s plugin)"), plugin->info->name); + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + + +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data = (PluginContext *) pdata; + + g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy); + g_free(data->help_text); +} + + +static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata) +{ + /* We know the extension is right (Geany checks that). For demo purposes we perform an + * additional check. This is not necessary when the extension is unique enough. */ + gboolean match = FALSE; + gchar linebuf[128]; + FILE *f = fopen(filename, "r"); + if (f != NULL) + { + if (fgets(linebuf, sizeof(linebuf), f) != NULL) + match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n"); + fclose(f); + } + return match ? PROXY_MATCHED : PROXY_IGNORED; +} + + +static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin, + const gchar *filename, gpointer pdata) +{ + GKeyFile *file; + gboolean result; + + file = g_key_file_new(); + result = g_key_file_load_from_file(file, filename, 0, NULL); + + if (result) + { + PluginContext *data = g_new0(PluginContext, 1); + data->file = file; + + plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL); + plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL); + plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL); + plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL); + + plugin->funcs->init = proxy_init; + plugin->funcs->help = proxy_help; + plugin->funcs->cleanup = proxy_cleanup; + + /* Cannot pass g_free as free_func be Geany calls it before unloading, and since + * demoproxy_unload() accesses the data this would be catastrophic */ + GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL); + return data; + } + + g_key_file_free(file); + return NULL; +} + + +static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata) +{ + PluginContext *data = load_data; + + g_free((gchar *)plugin->info->name); + g_free((gchar *)plugin->info->description); + g_free((gchar *)plugin->info->version); + g_free((gchar *)plugin->info->author); + + g_key_file_free(data->file); + g_free(data); +} + + +/* Called by Geany to initialize the plugin. */ +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + const gchar *extensions[] = { "ini", "px", NULL }; + + plugin->proxy_funcs->probe = demoproxy_probe; + plugin->proxy_funcs->load = demoproxy_load; + plugin->proxy_funcs->unload = demoproxy_unload; + + return geany_plugin_register_proxy(plugin, extensions); +} + + +/* Called by Geany before unloading the plugin. */ +static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data) +{ +} + + +G_MODULE_EXPORT +void geany_load_module(GeanyPlugin *plugin) +{ + plugin->info->name = _("Demo Proxy"); + plugin->info->description = _("Example Proxy."); + plugin->info->version = "0.1"; + plugin->info->author = _("The Geany developer team"); + + plugin->funcs->init = demoproxy_init; + plugin->funcs->cleanup = demoproxy_cleanup; + + GEANY_PLUGIN_REGISTER(plugin, 225); +}
Modified: plugins/demoproxytest.px 15 lines changed, 15 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,15 @@ +#!!PLUXY_MAGIC!! + +[Init] +item0 = Bam +item1 = Foo +item2 = Bar + +[Help] +text = I'm a simple test. Nothing to see! + +[Info] +name = Demo Pluxy Tester +description = I'm a simple test. Nothing to see! +version = 0.1 +author = The Geany developer team
Modified: po/POTFILES.skip 1 lines changed, 1 insertions(+), 0 deletions(-) =================================================================== @@ -6,5 +6,6 @@ geany.desktop.in geany.glade # no need to translate these files plugins/demoplugin.c +plugins/demoproxy.c doc/stash-example.c doc/stash-gui-example.c
Modified: src/plugindata.h 56 lines changed, 54 insertions(+), 2 deletions(-) =================================================================== @@ -58,7 +58,7 @@ G_BEGIN_DECLS * @warning You should not test for values below 200 as previously * @c GEANY_API_VERSION was defined as an enum value, not a macro. */ -#define GEANY_API_VERSION 225 +#define GEANY_API_VERSION 226
/* hack to have a different ABI when built with GTK3 because loading GTK2-linked plugins * with GTK3-linked Geany leads to crash */ @@ -240,6 +240,7 @@ GeanyData; #define geany geany_data /**< Simple macro for @c geany_data that reduces typing. */
typedef struct GeanyPluginFuncs GeanyPluginFuncs; +typedef struct GeanyProxyFuncs GeanyProxyFuncs;
/** Basic information for the plugin and identification. * @see geany_plugin. */ @@ -248,7 +249,8 @@ typedef struct GeanyPlugin PluginInfo *info; /**< Fields set in plugin_set_info(). */ GeanyData *geany_data; /**< Pointer to global GeanyData intance */ GeanyPluginFuncs *funcs; /**< Functions implemented by the plugin, set in geany_load_module() */ - + GeanyProxyFuncs *proxy_funcs; /**< Hooks implemented by the plugin if it wants to act as a proxy + Must be set prior to calling geany_plugin_register_proxy() */ struct GeanyPluginPrivate *priv; /* private */ } GeanyPlugin; @@ -347,6 +349,56 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr geany_plugin_register_full((plugin), GEANY_API_VERSION, \ (min_api_version), GEANY_ABI_VERSION, (pdata), (free_func))
+/** Return values for GeanyProxyHooks::probe() + * + * Only @c PROXY_IGNORED, @c PROXY_MATCHED or @c PROXY_MATCHED|PROXY_NOLOAD + * are valid return values. + * + * @see geany_plugin_register_proxy() for a full description of the proxy plugin mechanisms. + * + * @since 1.26 (API 226) + */ +typedef enum +{ + /** The proxy is not responsible at all, and Geany or other plugins are free + * to probe it. + **/ + PROXY_IGNORED, + /** The proxy is responsible for this file, and creates a plugin for it */ + PROXY_MATCHED, + + /** The proxy is does not directly load it, but it's still tied to the proxy + * + * This is for plugins that come in multiple files where only one of these + * files is relevant for the plugin creation (for the PM dialog). The other + * files should be ignored by Geany and other proxies. Example: libpeas has + * a .plugin and a .so per plugin. Geany should not process the .so file + * if there is a corresponding .plugin. + */ + PROXY_NOLOAD = 0x100, +} +GeanyProxyProbeResults; + + +/** Hooks that need to be implemented by every proxy + * + * @see geany_plugin_register_proxy() for a full description of the proxy mechanism. + * + * @since 1.26 (API 226) + **/ +struct GeanyProxyFuncs +{ + /** Called to determine whether the proxy is truly responsible for the requested plugin. + * A NULL pointer assumes the probe() function would always return @ref PROXY_MATCHED */ + gint (*probe) (GeanyPlugin *proxy, const gchar *filename, gpointer pdata); + /** Called after probe(), to perform the actual job of loading the plugin */ + gpointer (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata); + /** Called when the user initiates unloading of a plugin, e.g. on Geany exit */ + void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata); +}; + +gint geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions); + /* Deprecated aliases */ #ifndef GEANY_DISABLE_DEPRECATED
Modified: src/pluginprivate.h 16 lines changed, 12 insertions(+), 4 deletions(-) =================================================================== @@ -46,9 +46,10 @@ typedef enum _LoadedFlags { } LoadedFlags;
+typedef struct GeanyPluginPrivate Plugin; /* shorter alias */ + typedef struct GeanyPluginPrivate { - GModule *module; gchar *filename; /* plugin filename (/path/libname.so) */ PluginInfo info; /* plugin name, description, etc */ GeanyPlugin public; /* fields the plugin can read */ @@ -66,6 +67,14 @@ typedef struct GeanyPluginPrivate gpointer cb_data; /* user data passed back to functions in GeanyPluginFuncs */ GDestroyNotify cb_data_destroy; /* called when the plugin is unloaded, for cb_data */ LoadedFlags flags; /* bit-or of LoadedFlags */ + + /* proxy plugin support */ + GeanyProxyFuncs proxy_cbs; + Plugin *proxy; /* The proxy that handles this plugin */ + gpointer proxy_data; /* Data passed to the proxy hooks of above proxy, so + * this gives the proxy a pointer to each plugin */ + gint proxied_count; /* count of active plugins this provides a proxy for + * (a count because of possibly nested proxies) */ } GeanyPluginPrivate;
@@ -73,10 +82,9 @@ GeanyPluginPrivate; #define PLUGIN_IS_LEGACY(p) (((p)->flags & IS_LEGACY) != 0) #define PLUGIN_HAS_LOAD_DATA(p) (((p)->flags & LOAD_DATA) != 0)
-typedef GeanyPluginPrivate Plugin; /* shorter alias */ - - void plugin_watch_object(Plugin *plugin, gpointer object); +void plugin_make_resident(Plugin *plugin); +gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym);
G_END_DECLS
Modified: src/plugins.c 697 lines changed, 564 insertions(+), 133 deletions(-) =================================================================== @@ -75,8 +75,33 @@ static GtkWidget *menu_separator = NULL; static gchar *get_plugin_path(void); static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data);
-static GeanyData geany_data; +typedef struct { + gchar extension[8]; + Plugin *plugin; /* &builtin_so_proxy_plugin for native plugins */ +} PluginProxy; + + +static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, const gchar *filename, gpointer pdata); +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata); + +static Plugin builtin_so_proxy_plugin = { + .proxy_cbs = { + .load = plugin_load_gmodule, + .unload = plugin_unload_gmodule, + }, + /* rest of Plugin can be NULL/0 */ +};
+static PluginProxy builtin_so_proxy = { + .extension = G_MODULE_SUFFIX, + .plugin = &builtin_so_proxy_plugin, +}; + +static GQueue active_proxies = G_QUEUE_INIT; + +static void plugin_free(Plugin *plugin); + +static GeanyData geany_data;
static void geany_data_init(void) @@ -102,19 +127,42 @@ geany_data_init(void) }
+/* In order to have nested proxies work the count of dependent plugins must propagate up. + * This prevents that any plugin in the tree is unloaded while a leaf plugin is active. */ +static void proxied_count_inc(Plugin *proxy) +{ + do + { + proxy->proxied_count += 1; + proxy = proxy->proxy; + } while (proxy != NULL); +} + + +static void proxied_count_dec(Plugin *proxy) +{ + g_warn_if_fail(proxy->proxied_count > 0); + + do + { + proxy->proxied_count -= 1; + proxy = proxy->proxy; + } while (proxy != NULL); +} + + /* Prevent the same plugin filename being loaded more than once. * Note: g_module_name always returns the .so name, even when Plugin::filename is a .la file. */ static gboolean -plugin_loaded(GModule *module) +plugin_loaded(Plugin *plugin) { gchar *basename_module, *basename_loaded; GList *item;
- basename_module = g_path_get_basename(g_module_name(module)); + basename_module = g_path_get_basename(plugin->filename); for (item = plugin_list; item != NULL; item = g_list_next(item)) { - basename_loaded = g_path_get_basename( - g_module_name(((Plugin*)item->data)->module)); + basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename);
if (utils_str_equal(basename_module, basename_loaded)) { @@ -131,7 +179,7 @@ plugin_loaded(GModule *module) * would cause a crash. */ for (item = active_plugin_list; item != NULL; item = g_list_next(item)) { - basename_loaded = g_path_get_basename(g_module_name(((Plugin*)item->data)->module)); + basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename);
if (utils_str_equal(basename_module, basename_loaded)) { @@ -168,22 +216,27 @@ static Plugin *find_active_plugin_by_name(const gchar *filename) static gboolean plugin_check_version(Plugin *plugin, int plugin_version_code) { - GModule *module = plugin->module; + gboolean ret = TRUE; if (plugin_version_code < 0) { + gchar *name = g_path_get_basename(plugin->filename); msgwin_status_add(_("The plugin "%s" is not binary compatible with this " - "release of Geany - please recompile it."), g_module_name(module)); + "release of Geany - please recompile it."), name); geany_debug("Plugin "%s" is not binary compatible with this " - "release of Geany - recompile it.", g_module_name(module)); - return FALSE; + "release of Geany - recompile it.", name); + ret = FALSE; + g_free(name); } - if (plugin_version_code > GEANY_API_VERSION) + else if (plugin_version_code > GEANY_API_VERSION) { + gchar *name = g_path_get_basename(plugin->filename); geany_debug("Plugin "%s" requires a newer version of Geany (API >= v%d).", - g_module_name(module), plugin_version_code); - return FALSE; + name, plugin_version_code); + ret = FALSE; + g_free(name); } - return TRUE; + + return ret; }
@@ -217,9 +270,10 @@ static void read_key_group(Plugin *plugin) { GeanyKeyGroupInfo *p_key_info; GeanyKeyGroup **p_key_group; + GModule *module = plugin->proxy_data;
- g_module_symbol(plugin->module, "plugin_key_group_info", (void *) &p_key_info); - g_module_symbol(plugin->module, "plugin_key_group", (void *) &p_key_group); + g_module_symbol(module, "plugin_key_group_info", (void *) &p_key_info); + g_module_symbol(module, "plugin_key_group", (void *) &p_key_group); if (p_key_info && p_key_group) { GeanyKeyGroupInfo *key_info = p_key_info; @@ -307,8 +361,10 @@ gboolean geany_plugin_register(GeanyPlugin *plugin, gint api_version, gint min_a /* Only init and cleanup callbacks are truly mandatory. */ if (! cbs->init || ! cbs->cleanup) { - geany_debug("Plugin '%s' has no %s function - ignoring plugin!", - g_module_name(p->module), cbs->init ? "cleanup" : "init"); + gchar *name = g_path_get_basename(p->filename); + geany_debug("Plugin '%s' has no %s function - ignoring plugin!", name, + cbs->init ? "cleanup" : "init"); + g_free(name); } else { @@ -425,7 +481,7 @@ static void register_legacy_plugin(Plugin *plugin, GModule *module) if (! g_module_symbol(module, "plugin_" #__x, (void *) (&p_##__x))) \ { \ geany_debug("Plugin "%s" has no plugin_" #__x "() function - ignoring plugin!", \ - g_module_name(plugin->module)); \ + g_module_name(module)); \ return; \ } CHECK_FUNC(version_check); @@ -481,21 +537,23 @@ static gboolean plugin_load(Plugin *plugin) { gboolean init_ok = TRUE; + /* Start the plugin. Legacy plugins require additional cruft. */ - if (PLUGIN_IS_LEGACY(plugin)) + if (PLUGIN_IS_LEGACY(plugin) && plugin->proxy == &builtin_so_proxy_plugin) { GeanyPlugin **p_geany_plugin; PluginInfo **p_info; PluginFields **plugin_fields; + GModule *module = plugin->proxy_data; /* set these symbols before plugin_init() is called * we don't set geany_data since it is set directly by plugin_new() */ - g_module_symbol(plugin->module, "geany_plugin", (void *) &p_geany_plugin); + g_module_symbol(module, "geany_plugin", (void *) &p_geany_plugin); if (p_geany_plugin) *p_geany_plugin = &plugin->public; - g_module_symbol(plugin->module, "plugin_info", (void *) &p_info); + g_module_symbol(module, "plugin_info", (void *) &p_info); if (p_info) *p_info = &plugin->info; - g_module_symbol(plugin->module, "plugin_fields", (void *) &plugin_fields); + g_module_symbol(module, "plugin_fields", (void *) &plugin_fields); if (plugin_fields) *plugin_fields = &plugin->fields; read_key_group(plugin); @@ -525,26 +583,77 @@ plugin_load(Plugin *plugin) * keep list sorted so tools menu items and plugin preference tabs are * sorted by plugin name */ active_plugin_list = g_list_insert_sorted(active_plugin_list, plugin, cmp_plugin_names); + proxied_count_inc(plugin->proxy);
geany_debug("Loaded: %s (%s)", plugin->filename, plugin->info.name); return TRUE; }
+static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *fname, gpointer pdata) +{ + GModule *module; + void (*p_geany_load_module)(GeanyPlugin *); + + g_return_val_if_fail(g_module_supported(), NULL); + /* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime, + * causing a segfault. Without that flag the module will safely fail to load. + * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would + * otherwise not be detected due to the shadowing of Geany's app variable. + * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info() + * function from a plugin will be shadowed. */ + module = g_module_open(fname, G_MODULE_BIND_LOCAL); + if (!module) + { + geany_debug("Can't load plugin: %s", g_module_error()); + return NULL; + } + + /*geany_debug("Initializing plugin '%s'", plugin->info.name);*/ + g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module); + if (p_geany_load_module) + { + /* This is a new style plugin. It should fill in plugin->info and then call + * geany_plugin_register() in its geany_load_module() to successfully load. + * The ABI and API checks are performed by geany_plugin_register() (i.e. by us). + * We check the LOADED_OK flag separately to protect us against buggy plugins + * who ignore the result of geany_plugin_register() and register anyway */ + p_geany_load_module(subplugin); + } + else + { + /* This is the legacy / deprecated code path. It does roughly the same as + * geany_load_module() and geany_plugin_register() together for the new ones */ + register_legacy_plugin(subplugin->priv, module); + } + /* We actually check the LOADED_OK flag later */ + return module; +} + + +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata) +{ + GModule *module = (GModule *) load_data; + + g_return_if_fail(module != NULL); + + if (! g_module_close(module)) + g_warning("%s: %s", subplugin->priv->filename, g_module_error()); +} + + /* Load and optionally init a plugin. * load_plugin decides whether the plugin's plugin_init() function should be called or not. If it is * called, the plugin will be started, if not the plugin will be read only (for the list of * available plugins in the plugin manager). * When add_to_list is set, the plugin will be added to the plugin manager's plugin_list. */ static Plugin* -plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) +plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add_to_list) { Plugin *plugin; - GModule *module; - void (*p_geany_load_module)(GeanyPlugin *);
g_return_val_if_fail(fname, NULL); - g_return_val_if_fail(g_module_supported(), NULL); + g_return_val_if_fail(proxy, NULL);
/* find the plugin in the list of already loaded, active plugins and use it, otherwise * load the module */ @@ -563,64 +672,48 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) return plugin; }
- /* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime, - * causing a segfault. Without that flag the module will safely fail to load. - * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would - * otherwise not be detected due to the shadowing of Geany's app variable. - * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info() - * function from a plugin will be shadowed. */ - module = g_module_open(fname, G_MODULE_BIND_LOCAL); - if (! module) - { - geany_debug("Can't load plugin: %s", g_module_error()); - return NULL; - } - - if (plugin_loaded(module)) - { - geany_debug("Plugin "%s" already loaded.", fname); - - if (! g_module_close(module)) - g_warning("%s: %s", fname, g_module_error()); - return NULL; - } - plugin = g_new0(Plugin, 1); - plugin->module = module; plugin->filename = g_strdup(fname); + plugin->proxy = proxy; plugin->public.geany_data = &geany_data; plugin->public.priv = plugin; /* Fields of plugin->info/funcs must to be initialized by the plugin */ plugin->public.info = &plugin->info; plugin->public.funcs = &plugin->cbs; + plugin->public.proxy_funcs = &plugin->proxy_cbs;
- g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module); - if (p_geany_load_module) + if (plugin_loaded(plugin)) { - /* This is a new style plugin. It should fill in plugin->info and then call - * geany_plugin_register() in its geany_load_module() to successfully load. - * The ABI and API checks are performed by geany_plugin_register() (i.e. by us). - * We check the LOADED_OK flag separately to protect us against buggy plugins - * who ignore the result of geany_plugin_register() and register anyway */ - p_geany_load_module(&plugin->public); - } - else - { - /* This is the legacy / deprecated code path. It does roughly the same as - * geany_load_module() and geany_plugin_register() together for the new ones */ - register_legacy_plugin(plugin, module); + geany_debug("Plugin "%s" already loaded.", fname); + goto err; }
+ /* Load plugin, this should read its name etc. It must also call + * geany_plugin_register() for the following PLUGIN_LOADED_OK condition */ + plugin->proxy_data = proxy->proxy_cbs.load(&proxy->public, &plugin->public, fname, proxy->cb_data); + if (! PLUGIN_LOADED_OK(plugin)) { geany_debug("Failed to load "%s" - ignoring plugin!", fname); goto err; }
+ /* The proxy assumes success, therefore we have to call unload from here + * on in case of errors */ if (EMPTY(plugin->info.name)) { geany_debug("No plugin name set for "%s" - ignoring plugin!", fname); - goto err; + goto err_unload; + } + + /* cb_data_destroy() frees plugin->cb_data. If that pointer also passed to unload() afterwards + * then that would become a use-after-free. Disallow this combination. If a proxy + * needs the same pointer it must not use a destroy func but free manually in its unload(). */ + if (plugin->proxy_data == proxy->cb_data && plugin->cb_data_destroy) + { + geany_debug("Proxy of plugin "%s" specified invalid data - ignoring plugin!", fname); + plugin->proxy_data = NULL; + goto err_unload; }
if (load_plugin && !plugin_load(plugin)) @@ -628,7 +721,7 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) /* Handle failing init same as failing to load for now. In future we * could present a informational UI or something */ geany_debug("Plugin failed to initialize "%s" - ignoring plugin!", fname); - goto err; + goto err_unload; }
if (add_to_list) @@ -636,11 +729,11 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list)
return plugin;
-err: +err_unload: if (plugin->cb_data_destroy) plugin->cb_data_destroy(plugin->cb_data); - if (! g_module_close(module)) - g_warning("%s: %s", fname, g_module_error()); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data); +err: g_free(plugin->filename); g_free(plugin); return NULL; @@ -712,6 +805,40 @@ static void remove_sources(Plugin *plugin) }
+/* Make the GModule backing plugin resident (if it's GModule-backed at all) */ +void plugin_make_resident(Plugin *plugin) +{ + if (plugin->proxy == &builtin_so_proxy_plugin) + { + g_return_if_fail(plugin->proxy_data != NULL); + g_module_make_resident(plugin->proxy_data); + } + else + g_warning("Skipping g_module_make_resident() for non-native plugin"); +} + + +/* Retrieve the address of a symbol sym located in plugin, if it's GModule-backed */ +gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym) +{ + gpointer symbol; + + if (plugin->proxy == &builtin_so_proxy_plugin) + { + g_return_val_if_fail(plugin->proxy_data != NULL, NULL); + if (g_module_symbol(plugin->proxy_data, sym, &symbol)) + return symbol; + else + g_warning("Failed to locate signal handler for '%s': %s", + sym, g_module_error()); + } + else /* TODO: Could possibly support this via a new proxy hook */ + g_warning("Failed to locate signal handler for '%s': Not supported for non-native plugins", + sym); + return NULL; +} + + static gboolean is_active_plugin(Plugin *plugin) { return (g_list_find(active_plugin_list, plugin) != NULL); @@ -747,32 +874,84 @@ plugin_cleanup(Plugin *plugin) plugin->cb_data_destroy = NULL; }
+ proxied_count_dec(plugin->proxy); geany_debug("Unloaded: %s", plugin->filename); }
+/* Remove all plugins that proxy is a proxy for from plugin_list (and free) */ +static void free_subplugins(Plugin *proxy) +{ + GList *item; + + item = plugin_list; + while (item) + { + GList *next = g_list_next(item); + if (proxy == ((Plugin *) item->data)->proxy) + { + /* plugin_free modifies plugin_list */ + plugin_free((Plugin *) item->data); + } + item = next; + } +} + + +/* Returns true if the removal was successful (=> never for non-proxies) */ +static gboolean unregister_proxy(Plugin *proxy) +{ + gboolean is_proxy = FALSE; + GList *node; + + /* Remove the proxy from the proxy list first. It might appear more than once (once + * for each extension), but if it doesn't appear at all it's not actually a proxy */ + foreach_list_safe(node, active_proxies.head) + { + PluginProxy *p = node->data; + if (p->plugin == proxy) + { + is_proxy = TRUE; + g_queue_delete_link(&active_proxies, node); + } + } + return is_proxy; +} + + +/* Cleanup a plugin and free all resources allocated on behalf of it. + * + * If the plugin is a proxy then this also takes special care to unload all + * subplugin loaded through it (make sure none of them is active!) */ static void plugin_free(Plugin *plugin) { + Plugin *proxy; + g_return_if_fail(plugin); - g_return_if_fail(plugin->module); + g_return_if_fail(plugin->proxy); + g_return_if_fail(plugin->proxied_count == 0);
+ proxy = plugin->proxy; + /* If this a proxy remove all depending subplugins. We can assume none of them is *activated* + * (but potentially loaded). Note that free_subplugins() might call us through recursion */ if (is_active_plugin(plugin)) + { + if (unregister_proxy(plugin)) + free_subplugins(plugin); plugin_cleanup(plugin); + }
active_plugin_list = g_list_remove(active_plugin_list, plugin); plugin_list = g_list_remove(plugin_list, plugin);
- /* cb_data_destroy might be plugin code and must be called before unloading the module */ + /* cb_data_destroy might be plugin code and must be called before unloading the module. */ if (plugin->cb_data_destroy) plugin->cb_data_destroy(plugin->cb_data); - - if (! g_module_close(plugin->module)) - g_warning("%s: %s", plugin->filename, g_module_error()); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data);
g_free(plugin->filename); g_free(plugin); - plugin = NULL; }
@@ -830,25 +1009,87 @@ static gboolean check_plugin_path(const gchar *fname) }
+/* Retuns NULL if this ain't a plugin, + * otherwise it returns the appropriate PluginProxy instance to load it */ +static PluginProxy* is_plugin(const gchar *file) +{ + GList *node; + const gchar *ext; + + /* extract file extension to avoid g_str_has_suffix() in the loop */ + ext = (const gchar *)strrchr(file, '.'); + if (ext == NULL) + return FALSE; + /* ensure the dot is really part of the filename */ + else if (strchr(ext, G_DIR_SEPARATOR) != NULL) + return FALSE; + + ext += 1; + /* O(n*m), (m being extensions per proxy) doesn't scale very well in theory + * but not a problem in practice yet */ + foreach_list(node, active_proxies.head) + { + PluginProxy *proxy = node->data; + if (utils_str_casecmp(ext, proxy->extension) == 0) + { + Plugin *p = proxy->plugin; + gint ret = PROXY_MATCHED; + + if (p->proxy_cbs.probe) + ret = p->proxy_cbs.probe(&p->public, file, p->cb_data); + switch (ret) + { + case PROXY_MATCHED: + return proxy; + case PROXY_MATCHED|PROXY_NOLOAD: + return NULL; + default: + if (ret != PROXY_IGNORED) + g_warning("Ignoring bogus return from proxy probe!\n"); + continue; + } + } + } + return NULL; +} + + /* load active plugins at startup */ static void load_active_plugins(void) { - guint i, len; + guint i, len, proxies;
if (active_plugins_pref == NULL || (len = g_strv_length(active_plugins_pref)) == 0) return;
- for (i = 0; i < len; i++) + /* If proxys are loaded we have to restart to load plugins that sort before their proxy */ + do { - const gchar *fname = active_plugins_pref[i]; - - if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS)) + proxies = active_proxies.length; + g_list_free_full(failed_plugins_list, (GDestroyNotify) g_free); + failed_plugins_list = NULL; + for (i = 0; i < len; i++) { - if (!check_plugin_path(fname) || plugin_new(fname, TRUE, FALSE) == NULL) - failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); + gchar *fname = active_plugins_pref[i]; + +#ifdef G_OS_WIN32 + /* ensure we have canonical paths */ + gchar *p = fname; + while ((p = strchr(p, '/')) != NULL) + *p = G_DIR_SEPARATOR; +#endif + + if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS)) + { + PluginProxy *proxy = NULL; + if (check_plugin_path(fname)) + proxy = is_plugin(fname); + if (proxy == NULL || plugin_new(proxy->plugin, fname, TRUE, FALSE) == NULL) + failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); + } } - } + } while (proxies != active_proxies.length); }
@@ -856,20 +1097,18 @@ static void load_plugins_from_path(const gchar *path) { GSList *list, *item; - gchar *fname, *tmp; gint count = 0;
list = utils_get_file_list(path, NULL, NULL);
for (item = list; item != NULL; item = g_slist_next(item)) { - tmp = strrchr(item->data, '.'); - if (tmp == NULL || utils_str_casecmp(tmp, "." G_MODULE_SUFFIX) != 0) - continue; + gchar *fname = g_build_filename(path, item->data, NULL); + PluginProxy *proxy = is_plugin(fname);
- fname = g_build_filename(path, item->data, NULL); - if (plugin_new(fname, FALSE, TRUE)) + if (proxy != NULL && plugin_new(proxy->plugin, fname, FALSE, TRUE)) count++; + g_free(fname); }
@@ -887,6 +1126,28 @@ static gchar *get_plugin_path(void) }
+/* See load_all_plugins(), this simply sorts items with lower hierarchy level first + * (where hierarchy level == number of intermediate proxies before the builtin so loader) */ +static gint cmp_plugin_by_proxy(gconstpointer a, gconstpointer b) +{ + const Plugin *pa = a; + const Plugin *pb = b; + + while (TRUE) + { + if (pa->proxy == pb->proxy) + return 0; + else if (pa->proxy == &builtin_so_proxy_plugin) + return -1; + else if (pb->proxy == &builtin_so_proxy_plugin) + return 1; + + pa = pa->proxy; + pb = pb->proxy; + } +} + + /* Load (but don't initialize) all plugins for the Plugin Manager dialog */ static void load_all_plugins(void) { @@ -911,6 +1172,13 @@ static void load_all_plugins(void) /* finally load plugins from $prefix/lib/geany */ load_plugins_from_path(plugin_path_system);
+ /* It is important to sort any plugins that are proxied after their proxy because + * pm_populate() needs the proxy to be loaded and active (if selected by user) in order + * to properly set the value for the PLUGIN_COLUMN_CAN_UNCHECK column. The order between + * sub-plugins does not matter, only between sub-plugins and their proxy, thus + * sorting by hierarchy level is perfectly sufficient */ + plugin_list = g_list_sort(plugin_list, cmp_plugin_by_proxy); + g_free(plugin_path_config); g_free(plugin_path_system); } @@ -1028,6 +1296,17 @@ void plugins_init(void)
g_signal_connect(geany_object, "save-settings", G_CALLBACK(update_active_plugins_pref), NULL); stash_group_add_string_vector(group, &active_plugins_pref, "active_plugins", NULL); + + g_queue_push_head(&active_proxies, &builtin_so_proxy); +} + + +/* Same as plugin_free(), except it does nothing for proxies-in-use, to be called on + * finalize in a loop */ +static void plugin_free_leaf(Plugin *p) +{ + if (p->proxied_count == 0) + plugin_free(p); }
@@ -1039,11 +1318,11 @@ void plugins_finalize(void) g_list_foreach(failed_plugins_list, (GFunc) g_free, NULL); g_list_free(failed_plugins_list); } - if (active_plugin_list != NULL) - { - g_list_foreach(active_plugin_list, (GFunc) plugin_free, NULL); - g_list_free(active_plugin_list); - } + /* Have to loop because proxys cannot be unloaded until after all their + * plugins are unloaded as well (the second loop should should catch all the remaining ones) */ + while (active_plugin_list != NULL) + g_list_foreach(active_plugin_list, (GFunc) plugin_free_leaf, NULL); + g_strfreev(active_plugins_pref); }
@@ -1072,6 +1351,7 @@ gboolean plugins_have_preferences(void) enum { PLUGIN_COLUMN_CHECK = 0, + PLUGIN_COLUMN_CAN_UNCHECK, PLUGIN_COLUMN_PLUGIN, PLUGIN_N_COLUMNS, PM_BUTTON_KEYBINDINGS, @@ -1083,7 +1363,7 @@ typedef struct { GtkWidget *dialog; GtkWidget *tree; - GtkListStore *store; + GtkTreeStore *store; GtkWidget *filter_entry; GtkWidget *configure_button; GtkWidget *keybindings_button; @@ -1137,6 +1417,27 @@ static void pm_selection_changed(GtkTreeSelection *selection, gpointer user_data }
+static gboolean find_iter_for_plugin(Plugin *p, GtkTreeModel *model, GtkTreeIter *iter) +{ + Plugin *pp; + gboolean valid; + + for (valid = gtk_tree_model_get_iter_first(model, iter); + valid; + valid = gtk_tree_model_iter_next(model, iter)) + { + gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &pp, -1); + if (p == pp) + return TRUE; + } + + return FALSE; +} + + +static void pm_populate(GtkTreeStore *store); + + static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer data) { gboolean old_state, state; @@ -1146,9 +1447,10 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer GtkTreePath *path = gtk_tree_path_new_from_string(pth); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree)); Plugin *p; + Plugin *proxy; + guint prev_num_proxies;
gtk_tree_model_get_iter(model, &iter, path); - gtk_tree_path_free(path);
gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_CHECK, &old_state, @@ -1156,15 +1458,20 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer
/* no plugins item */ if (p == NULL) + { + gtk_tree_path_free(path); return; + }
gtk_tree_model_filter_convert_iter_to_child_iter( GTK_TREE_MODEL_FILTER(model), &store_iter, &iter);
state = ! old_state; /* toggle the state */
- /* save the filename of the plugin */ + /* save the filename and proxy of the plugin */ file_name = g_strdup(p->filename); + proxy = p->proxy; + prev_num_proxies = active_proxies.length;
/* unload plugin module */ if (!state) @@ -1174,11 +1481,11 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer plugin_free(p);
/* reload plugin module and initialize it if item is checked */ - p = plugin_new(file_name, state, TRUE); + p = plugin_new(proxy, file_name, state, TRUE); if (!p) { /* plugin file may no longer be on disk, or is now incompatible */ - gtk_list_store_remove(pm_widgets.store, &store_iter); + gtk_tree_store_remove(pm_widgets.store, &store_iter); } else { @@ -1186,16 +1493,89 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer keybindings_load_keyfile(); /* load shortcuts */
/* update model */ - gtk_list_store_set(pm_widgets.store, &store_iter, + gtk_tree_store_set(pm_widgets.store, &store_iter, PLUGIN_COLUMN_CHECK, state, PLUGIN_COLUMN_PLUGIN, p, -1);
/* set again the sensitiveness of the configure and help buttons */ pm_update_buttons(p); + + /* Depending on the state disable the checkbox for the proxy of this plugin, and + * only re-enable if the proxy is not used by any other plugin */ + if (p->proxy != &builtin_so_proxy_plugin) + { + GtkTreeIter parent; + gboolean can_uncheck; + GtkTreePath *store_path = gtk_tree_model_filter_convert_path_to_child_path( + GTK_TREE_MODEL_FILTER(model), path); + + g_warn_if_fail(store_path != NULL); + if (gtk_tree_path_up(store_path)) + { + gtk_tree_model_get_iter(GTK_TREE_MODEL(pm_widgets.store), &parent, store_path); + + if (state) + can_uncheck = FALSE; + else + can_uncheck = p->proxy->proxied_count == 0; + + gtk_tree_store_set(pm_widgets.store, &parent, + PLUGIN_COLUMN_CAN_UNCHECK, can_uncheck, -1); + } + gtk_tree_path_free(store_path); + } } + /* We need to find out if a proxy was added or removed because that affects the plugin list + * presented by the plugin manager */ + if (prev_num_proxies != active_proxies.length) + { + /* Rescan the plugin list as we now support more. Gives some "already loaded" warnings + * they are unproblematic */ + if (prev_num_proxies < active_proxies.length) + load_all_plugins(); + + pm_populate(pm_widgets.store); + gtk_tree_view_expand_row(GTK_TREE_VIEW(pm_widgets.tree), path, FALSE); + } + + gtk_tree_path_free(path); g_free(file_name); }
+static void pm_populate(GtkTreeStore *store) +{ + GtkTreeIter iter; + GList *list; + + gtk_tree_store_clear(store); + list = g_list_first(plugin_list); + if (list == NULL) + { + gtk_tree_store_append(store, &iter, NULL); + gtk_tree_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, + PLUGIN_COLUMN_PLUGIN, NULL, -1); + } + else + { + for (; list != NULL; list = list->next) + { + Plugin *p = list->data; + GtkTreeIter parent; + + if (p->proxy != &builtin_so_proxy_plugin + && find_iter_for_plugin(p->proxy, GTK_TREE_MODEL(pm_widgets.store), &parent)) + gtk_tree_store_append(store, &iter, &parent); + else + gtk_tree_store_append(store, &iter, NULL); + + gtk_tree_store_set(store, &iter, + PLUGIN_COLUMN_CHECK, is_active_plugin(p), + PLUGIN_COLUMN_PLUGIN, p, + PLUGIN_COLUMN_CAN_UNCHECK, (p->proxied_count == 0), + -1); + } + } +}
static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data) @@ -1204,26 +1584,33 @@ static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y, GtkTreeIter iter; GtkTreePath *path; Plugin *p = NULL; + gboolean can_uncheck = TRUE;
if (! gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_mode, &model, &path, &iter)) return FALSE;
- gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, -1); + gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, PLUGIN_COLUMN_CAN_UNCHECK, &can_uncheck, -1); if (p != NULL) { - gchar *markup; - gchar *details; + gchar *prefix, *suffix, *details, *markup; + const gchar *uchk;
+ uchk = can_uncheck ? + "" : _("\n<i>Other plugins depend on this. Disable them first to allow deactivation.</i>\n"); + /* Four allocations is less than ideal but meh */ details = g_strdup_printf(_("Version:\t%s\nAuthor(s):\t%s\nFilename:\t%s"), p->info.version, p->info.author, p->filename); - markup = g_markup_printf_escaped("<b>%s</b>\n%s\n<small><i>\n%s</i></small>", - p->info.name, p->info.description, details); + prefix = g_markup_printf_escaped("<b>%s</b>\n%s\n", p->info.name, p->info.description); + suffix = g_markup_printf_escaped("<small><i>\n%s</i></small>", details); + markup = g_strconcat(prefix, uchk, suffix, NULL);
gtk_tooltip_set_markup(tooltip, markup); gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path);
g_free(details); + g_free(suffix); + g_free(prefix); g_free(markup); } gtk_tree_path_free(path); @@ -1328,6 +1715,9 @@ static gboolean pm_tree_filter_func(GtkTreeModel *model, GtkTreeIter *iter, gpoi gchar *haystack, *filename;
gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &plugin, -1); + + if (!plugin) + return FALSE; key = gtk_entry_get_text(GTK_ENTRY(pm_widgets.filter_entry));
filename = g_path_get_basename(plugin->filename); @@ -1356,13 +1746,11 @@ static void on_pm_tree_filter_entry_icon_release_cb(GtkEntry *entry, GtkEntryIco }
-static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) +static void pm_prepare_treeview(GtkWidget *tree, GtkTreeStore *store) { GtkCellRenderer *text_renderer, *checkbox_renderer; GtkTreeViewColumn *column; GtkTreeModel *filter_model; - GtkTreeIter iter; - GList *list; GtkTreeSelection *sel;
g_signal_connect(tree, "query-tooltip", G_CALLBACK(pm_treeview_query_tooltip), NULL); @@ -1371,7 +1759,8 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
checkbox_renderer = gtk_cell_renderer_toggle_new(); column = gtk_tree_view_column_new_with_attributes( - _("Active"), checkbox_renderer, "active", PLUGIN_COLUMN_CHECK, NULL); + _("Active"), checkbox_renderer, + "active", PLUGIN_COLUMN_CHECK, "activatable", PLUGIN_COLUMN_CAN_UNCHECK, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); g_signal_connect(checkbox_renderer, "toggled", G_CALLBACK(pm_plugin_toggled), NULL);
@@ -1396,27 +1785,6 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
g_signal_connect(tree, "button-press-event", G_CALLBACK(pm_treeview_button_press_cb), NULL);
- list = g_list_first(plugin_list); - if (list == NULL) - { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, - PLUGIN_COLUMN_PLUGIN, NULL, -1); - } - else - { - Plugin *p; - for (; list != NULL; list = list->next) - { - p = list->data; - - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - PLUGIN_COLUMN_CHECK, is_active_plugin(p), - PLUGIN_COLUMN_PLUGIN, p, - -1); - } - } /* filter */ filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); gtk_tree_model_filter_set_visible_func( @@ -1424,8 +1792,9 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
/* set model to tree view */ gtk_tree_view_set_model(GTK_TREE_VIEW(tree), filter_model); - g_object_unref(store); g_object_unref(filter_model); + + pm_populate(store); }
@@ -1533,9 +1902,11 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data)
/* prepare treeview */ pm_widgets.tree = gtk_tree_view_new(); - pm_widgets.store = gtk_list_store_new( - PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_POINTER); + pm_widgets.store = gtk_tree_store_new( + PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_POINTER); pm_prepare_treeview(pm_widgets.tree, pm_widgets.store); + gtk_tree_view_expand_all(GTK_TREE_VIEW(pm_widgets.tree)); + g_object_unref(pm_widgets.store);
swin = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin), @@ -1584,4 +1955,64 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data) }
+/** Register the plugin as a proxy for other plugins + * + * Proxy plugins register a list of file extensions and a set of callbacks that are called + * appropriately. A plugin can be a proxy for multiple types of sub-plugins by handling + * separate file extensions, however they must share the same set of hooks, because this + * function can only be called at most once per plugin. + * + * Each callback receives the plugin-defined data as parameter (see geany_plugin_register()). The + * callbacks must be set prior to calling this, by assigning to @a plugin->proxy_funcs. + * GeanyProxyFuncs::load and GeanyProxyFuncs::unload must be implemented. + * + * Nested proxies are unsupported at this point (TODO). + * + * @note It is entirely up to the proxy to provide access to Geany's plugin API. Native code + * can naturally call Geany's API directly, for interpreted languages the proxy has to + * implement some kind of bindings that the plugin can use. + * + * @see proxy for detailed documentation and an example. + * + * @param plugin The pointer to the plugin's GeanyPlugin instance + * @param extensions A @c NULL-terminated string array of file extensions, excluding the dot. + * @return @c TRUE if the proxy was successfully registered, otherwise @c FALSE + * + * @since 1.26 (API 226) + */ +GEANY_API_SYMBOL +gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions) +{ + Plugin *p; + const gchar **ext; + PluginProxy *proxy; + GList *node; + + g_return_val_if_fail(plugin != NULL, FALSE); + g_return_val_if_fail(extensions != NULL, FALSE); + g_return_val_if_fail(*extensions != NULL, FALSE); + g_return_val_if_fail(plugin->proxy_funcs->load != NULL, FALSE); + g_return_val_if_fail(plugin->proxy_funcs->unload != NULL, FALSE); + + p = plugin->priv; + /* Check if this was called aready. We want to reserve for the use case of calling + * this again to set new supported extensions (for example, based on proxy configuration). */ + foreach_list(node, active_proxies.head) + { + proxy = node->data; + g_return_if_fail(p != proxy->plugin); + } + + foreach_strv(ext, extensions) + { + proxy = g_new(PluginProxy, 1); + g_strlcpy(proxy->extension, *ext, sizeof(proxy->extension)); + proxy->plugin = p; + /* prepend, so that plugins automatically override core providers for a given extension */ + g_queue_push_head(&active_proxies, proxy); + } + + return TRUE; +} + #endif
Modified: src/pluginutils.c 11 lines changed, 2 insertions(+), 9 deletions(-) =================================================================== @@ -96,8 +96,7 @@ GEANY_API_SYMBOL void plugin_module_make_resident(GeanyPlugin *plugin) { g_return_if_fail(plugin); - - g_module_make_resident(plugin->priv->module); + plugin_make_resident(plugin->priv); }
@@ -444,12 +443,7 @@ static void connect_plugin_signals(GtkBuilder *builder, GObject *object, gpointer symbol = NULL; struct BuilderConnectData *data = user_data;
- if (!g_module_symbol(data->plugin->priv->module, handler_name, &symbol)) - { - g_warning("Failed to locate signal handler for '%s': %s", - signal_name, g_module_error()); - return; - } + symbol = plugin_get_module_symbol(data->plugin->priv, handler_name);
plugin_signal_connect(data->plugin, object, signal_name, FALSE, G_CALLBACK(symbol) /*ub?*/, data->user_data); @@ -503,7 +497,6 @@ void plugin_builder_connect_signals(GeanyPlugin *plugin, struct BuilderConnectData data = { NULL };
g_return_if_fail(plugin != NULL && plugin->priv != NULL); - g_return_if_fail(plugin->priv->module != NULL); g_return_if_fail(GTK_IS_BUILDER(builder));
data.user_data = user_data;
Modified: src/utils.h 8 lines changed, 8 insertions(+), 0 deletions(-) =================================================================== @@ -116,6 +116,14 @@ G_BEGIN_DECLS #define foreach_slist(node, list) \ foreach_list(node, list)
+/* Iterates all the nodes in @a list. Safe against removal during iteration + * @param node should be a (@c GList*). + * @param list @c GList to traverse. */ +#define foreach_list_safe(node, list) \ + for (GList *_node = (list), *_next = (list) ? (list)->next : NULL; \ + (node = _node) != NULL; \ + _node = _next, _next = _next ? _next->next : NULL) + /** Iterates through each unsorted filename in a @c GDir. * @param filename (@c const @c gchar*) locale-encoded filename, without path. Do not modify or free. * @param dir @c GDir created with @c g_dir_open(). Call @c g_dir_close() afterwards.
-------------- This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).