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 @@
+