diff --git a/docs/plugin-api-additions/compatibility.md b/docs/plugin-api-additions/compatibility.md new file mode 100644 index 00000000..4c740d51 --- /dev/null +++ b/docs/plugin-api-additions/compatibility.md @@ -0,0 +1,24 @@ +# Optional plugin API additions (discussion #144) + +Existing hooks, callbacks, return values, context lookup, strings and menu behaviour +remain unchanged. The C plugin function table gains four fixed append-only slots; +unused slots are NULL. Older binaries use the unchanged prefix. A plugin must check +the documented get_info() feature key before accessing a new slot on an older host. +New Python modules are installed by Meson and the existing Windows copy/installer +pipeline. Only modules present in the installation are exported; imports of +zoitechat, hexchat and xchat retain their compatibility aliases. + +All APIs run on the client main thread unless explicitly documented otherwise. +New reserved flags must be zero. These changes do not make old API calls thread-safe. + +Nested hook dispatch defers deleted-hook reclamation until its outermost return, +so new observers can safely emit events and unhook themselves or other hooks. + +The optional asyncio module requires Python 3.7 or newer; older supported +interpreters skip that module and retain the existing scripting API. + +Python reload drops the script/API modules while retaining the embedding module +so callback and lib.ph pointer types keep the same CFFI identity. + +Server-hook insertion matches both raw and attrs hooks so documented priorities +are honoured for mixed registrations, including new filtered attrs callbacks. diff --git a/plugins/python/_zoitechat.py b/plugins/python/_zoitechat.py index 74739b6e..b60a5e35 100644 --- a/plugins/python/_zoitechat.py +++ b/plugins/python/_zoitechat.py @@ -383,3 +383,24 @@ def find_context(server=None, channel=None): return None return Context(ctx) + + +# Optional additive APIs live in separate modules so builds can select features. +def __load_additive_features(): + import importlib + import os + + for suffix in ('fd', 'events', 'async', 'names', 'contexts', + 'post', 'filters', 'caps'): + if suffix == 'async' and sys.version_info < (3, 7): + continue + name = '_zoitechat_' + suffix + if not os.path.isfile(os.path.join(os.path.dirname(__file__), name + '.py')): + continue + module = importlib.import_module(name) + for symbol in module.__all__: + globals()[symbol] = getattr(module, symbol) + __all__.append(symbol) + + +__load_additive_features() diff --git a/plugins/python/meson.build b/plugins/python/meson.build index e1751e02..d42c37d8 100644 --- a/plugins/python/meson.build +++ b/plugins/python/meson.build @@ -27,7 +27,11 @@ python3_source = custom_target('python-bindings', command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@'] ) -install_data(['_zoitechat.py', 'zoitechat.py', 'xchat.py', 'hexchat.py'], +# Include any selected additive API modules alongside the compatibility modules. +python_api_files = run_command(python3, '-c', + 'import glob,sys; print("\\n".join(sorted(glob.glob(sys.argv[1] + "/_zoitechat*.py"))))', + meson.current_source_dir(), check: true).stdout().strip().split('\n') +install_data(python_api_files + ['zoitechat.py', 'xchat.py', 'hexchat.py'], install_dir: join_paths(get_option('libdir'), 'zoitechat/python') ) diff --git a/plugins/python/python.py b/plugins/python/python.py index c60a6aa5..4b55ca98 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -590,7 +590,12 @@ def _on_plugin_deinit(): sys.stderr = sys.__stderr__ pydoc.help = pydoc.Helper() - for mod in ('_zoitechat', 'zoitechat', 'xchat', '_zoitechat_embedded'): + # CFFI callbacks keep the embedding module's ffi/lib identity for the + # process lifetime; deleting it creates incompatible types on reload. + for mod in ('_zoitechat', 'zoitechat', 'hexchat', 'xchat', + '_zoitechat_fd', '_zoitechat_events', '_zoitechat_async', + '_zoitechat_names', '_zoitechat_contexts', '_zoitechat_post', + '_zoitechat_filters', '_zoitechat_caps'): try: del sys.modules[mod] diff --git a/plugins/python/python3.vcxproj b/plugins/python/python3.vcxproj index d183a285..6feba850 100644 --- a/plugins/python/python3.vcxproj +++ b/plugins/python/python3.vcxproj @@ -52,6 +52,7 @@ + diff --git a/plugins/python/python38.vcxproj b/plugins/python/python38.vcxproj index 688de715..e2f10ab8 100644 --- a/plugins/python/python38.vcxproj +++ b/plugins/python/python38.vcxproj @@ -52,6 +52,7 @@ + diff --git a/src/common/plugin.c b/src/common/plugin.c index ba6528bf..07c08a58 100644 --- a/src/common/plugin.c +++ b/src/common/plugin.c @@ -176,6 +176,7 @@ enum GSList *plugin_list = NULL; /* export for plugingui.c */ static GSList *hook_list = NULL; +static unsigned int plugin_dispatch_depth; extern const struct prefs vars[]; /* cfgfiles.c */ @@ -243,7 +244,7 @@ plugin_list_add (zoitechat_context *ctx, char *filename, const char *name, { zoitechat_plugin *pl; - pl = g_new (zoitechat_plugin, 1); + pl = g_new0 (zoitechat_plugin, 1); pl->handle = handle; pl->filename = filename; pl->context = ctx; @@ -610,6 +611,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[], zoitechat_hook *hook; int ret, eat = 0; + plugin_dispatch_depth++; list = hook_list; while (1) { @@ -655,6 +657,10 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[], } xit: + /* Nested callbacks can unhook entries still used by an outer dispatch. + * Keep both hook objects and list links alive until the outermost return. */ + if (--plugin_dispatch_depth != 0) + return eat; /* really remove deleted hooks now */ list = hook_list; while (list) @@ -806,7 +812,7 @@ plugin_insert_hook (zoitechat_hook *new_hook) break; case HOOK_SERVER: case HOOK_SERVER_ATTRS: - new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS; + new_hook_type = HOOK_SERVER | HOOK_SERVER_ATTRS; break; default: new_hook_type = new_hook->type; diff --git a/src/common/plugin.h b/src/common/plugin.h index 948346fe..d4ef3334 100644 --- a/src/common/plugin.h +++ b/src/common/plugin.h @@ -150,6 +150,15 @@ struct _zoitechat_plugin void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph, zoitechat_event_attrs *attrs); + /* Additive API slots: append only; old binary offsets are unchanged. */ + zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int, + int, int (*)(char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int, + int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int, + int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *); + /* PRIVATE FIELDS! */ void *handle; /* from dlopen */ char *filename; /* loaded from */ diff --git a/src/common/zoitechat-plugin.h b/src/common/zoitechat-plugin.h index f0929816..e7863238 100644 --- a/src/common/zoitechat-plugin.h +++ b/src/common/zoitechat-plugin.h @@ -193,6 +193,15 @@ struct _zoitechat_plugin zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph); void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph, zoitechat_event_attrs *attrs); + /* Additive API slots: append only; old binary offsets are unchanged. */ + zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int, + int, int (*)(char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int, + int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int, + int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *); + zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *); + }; #endif diff --git a/win32/copy/copy.vcxproj b/win32/copy/copy.vcxproj index e5f3730b..2cf11e2c 100644 --- a/win32/copy/copy.vcxproj +++ b/win32/copy/copy.vcxproj @@ -90,6 +90,7 @@ + @@ -115,6 +116,7 @@ +