mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 17:01:11 +00:00
Two changes to the same surface, both about the by-name teardown hook this repo's qt-host-generator emits. 1. logos::runPluginAboutToUnload(), ported verbatim from logos-module-loader-qt's logos_host.cpp, where it was a file-local helper. ui-host (logos-view-module-runtime) needs the identical algorithm before `delete pluginObject`, and a second hand-written copy of a nested event loop plus deadline is not a thing to have twice. Both load-bearing properties are intact: the hook is invoked BY NAME rather than through the vtable (PluginInterface is compiled separately into every plugin, so a new virtual would shift the vtable under every plugin already built), and `deadline.isActive()` remains the only bit distinguishing "finished" from "gave up". The grace CONSTANT deliberately does not move with it. It stays a parameter because the two hosts have different budgets: the container gives logos_host 5000ms and it carves out 3000, while ViewModuleHost::stop() gives ui-host 3000ms total. Porting the constant would have consumed ui-host's entire budget and hard-killed every async view mid-teardown — exactly what the hook exists to prevent. It lives HERE because this repo emits the surface it reaches. Both halves under one roof is what makes checks.unload-contract possible: it generates real glue, scrapes the symbol names out of it, and greps the consumer for those same names. Nothing in either build tied them together before. 2. The two version guards this generator emits tested LOGOS_PROTOCOL_VERSION_MINOR and ignored LOGOS_PROTOCOL_VERSION_MAJOR. At protocol 1.0.0 the MINOR resets to 0, both go false, and the glue stops emitting the grant_host_services call (0.3) and the teardown pair (0.5). Not a link error and not a dlopen failure: logos-cpp-sdk guards the matching DEFINITIONS the same way, so the calls and the definitions disappear together and everything still builds and loads. Modules just quietly stop having teardown. logos-rust-sdk would keep emitting all ten exports, because it compares the (major, minor) tuple — so the two sides would silently disagree. The arithmetic is emitted expanded rather than behind a function-like macro, because the generated sources are resolved by unifdef in logos-cpp-sdk's ABI check and unifdef silently no-ops on what it cannot parse. Matching logos-cpp-sdk#145 character for character. unload-contract, qt-host-generator, qt-host and vanilla-plugin all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.8 KiB
C++
72 lines
2.8 KiB
C++
#include "logos_plugin_unload.h"
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QEventLoop>
|
|
#include <QMetaObject>
|
|
#include <QObject>
|
|
#include <QTimer>
|
|
#include <QtGlobal>
|
|
|
|
namespace logos {
|
|
|
|
// Invoked BY NAME, not through the vtable, and that is the whole reason this is
|
|
// shaped the way it is. `PluginInterface` is compiled into every module .so
|
|
// separately; adding a virtual to it would shift the vtable under every plugin
|
|
// already built and turn a missing hook into undefined behaviour instead of a
|
|
// no-op. `initLogos` is delivered the same way for the same reason.
|
|
//
|
|
// A plugin that does not declare the hook simply has no such meta-method:
|
|
// invokeMethod returns false, we log nothing and move on. That is the common
|
|
// case and it must stay free.
|
|
void runPluginAboutToUnload(QObject* plugin, int graceMs)
|
|
{
|
|
if (!plugin) return;
|
|
|
|
int flag = 0; // LogosShutdown::Synchronous
|
|
if (!QMetaObject::invokeMethod(plugin, "aboutToUnload",
|
|
Qt::DirectConnection, Q_RETURN_ARG(int, flag))) {
|
|
return; // module predates the hook, or does not want it
|
|
}
|
|
if (flag == 0) return; // Synchronous: already quiescent
|
|
|
|
// Asynchronous: the module is finishing. Run a nested event loop rather
|
|
// than sleeping -- unloadFinished() may arrive as a queued event from a
|
|
// worker thread, and a module doing its last work almost certainly needs
|
|
// the loop running to do it. The signal is reached by NAME for the same
|
|
// ABI reason as the hook itself.
|
|
QElapsedTimer elapsed;
|
|
elapsed.start();
|
|
|
|
QEventLoop loop;
|
|
QTimer deadline;
|
|
deadline.setSingleShot(true);
|
|
const bool connected = QObject::connect(plugin, SIGNAL(unloadFinished()),
|
|
&loop, SLOT(quit()));
|
|
if (!connected) {
|
|
// The module said Asynchronous but exposes no way to say it is done.
|
|
// Waiting out the full grace period for a signal that cannot arrive
|
|
// helps nobody.
|
|
qWarning("module returned Asynchronous from aboutToUnload() but has no "
|
|
"unloadFinished() signal; not waiting");
|
|
return;
|
|
}
|
|
QObject::connect(&deadline, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
deadline.start(graceMs);
|
|
loop.exec();
|
|
|
|
// Still armed means the loop was quit by the signal rather than by the
|
|
// deadline -- the one bit that separates "finished" from "gave up".
|
|
const bool finished = deadline.isActive();
|
|
deadline.stop();
|
|
|
|
if (!finished) {
|
|
// Loud, because it costs every teardown of this module the full grace
|
|
// period and the module is the only thing that can fix it.
|
|
qWarning("module did not finish unloading within %dms; proceeding", graceMs);
|
|
} else {
|
|
qDebug("module finished unloading in %lldms", (long long)elapsed.elapsed());
|
|
}
|
|
}
|
|
|
|
} // namespace logos
|