File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 11#pragma once
22
33#include < filesystem>
4+ #include < functional>
45#include < memory>
56#include < string_view>
67#include < vector>
@@ -125,6 +126,24 @@ namespace srt::core {
125126 // / specs, (3) call \c ModelRegistry::unbindPackage(pkgId).
126127 Expected<void > unloadPackage (const std::filesystem::path &path);
127128
129+ // --- Destruction callbacks (DLL unload ordering) ---
130+ // Register a callback to be invoked in ~Runtime(), BEFORE PluginFactory
131+ // unloads plugin DLLs. This allows subsystems that hold shared_ptrs to
132+ // plugin-DLL-resident objects (e.g. srt::g2p::Manager holding Task/
133+ // SessionFactory instances whose vtables live in srt-driver-onnx.dll)
134+ // to release those references while the plugin DLLs are still loaded.
135+ //
136+ // The callback executes during Runtime destruction, which happens
137+ // BEFORE static singleton destructors run. Without this hook, a static
138+ // singleton (e.g. srt::g2p::Manager) would release its shared_ptrs in
139+ // its own destructor (after Runtime destruction), accessing freed
140+ // plugin DLL memory through dangling vtable pointers.
141+ //
142+ // Callbacks run in reverse order of registration (LIFO). The callback
143+ // MUST NOT capture a shared_ptr to the Runtime itself (would create a
144+ // cycle); capture raw pointers or weak_ptr instead.
145+ void addDestructionCallback (std::function<void ()> callback);
146+
128147 protected:
129148 static void registerModuleCategoryFactory (ModuleCategory *(*fac)(Runtime *));
130149
Original file line number Diff line number Diff line change @@ -119,6 +119,17 @@ namespace srt::g2p {
119119 std::vector<srt::dependency::ModuleMetadata> getModuleMetadatas (
120120 const srt::core::ContextKey &ctxKey);
121121
122+ // / Release all object references held by category ObjectPools and
123+ // / clear loaded packages. Called automatically by setupG2pOnnxDriver's
124+ // / Runtime destruction callback, so manual invocation is typically
125+ // / unnecessary.
126+ // /
127+ // / This is the counterpart to the constructor: it performs the same
128+ // / cleanup as the destructor but can be called early (before plugin
129+ // / DLLs are unloaded) to prevent use-after-free when static singleton
130+ // / destructors run after Runtime destruction.
131+ void shutdown ();
132+
122133 protected:
123134 class Impl ;
124135 std::unique_ptr<Impl> _impl;
Original file line number Diff line number Diff line change @@ -39,6 +39,19 @@ namespace srt::core {
3939 }
4040
4141 Runtime::Impl::~Impl () {
42+ // Run destruction callbacks in LIFO order BEFORE deleting module
43+ // categories or unloading plugin DLLs (the PluginFactory is owned by
44+ // m_services, which is destroyed after this destructor body runs as
45+ // part of member destruction). This lets subsystems release shared_ptrs
46+ // to plugin-DLL-resident objects while the DLLs are still loaded.
47+ for (auto it = m_destructionCallbacks.rbegin ();
48+ it != m_destructionCallbacks.rend (); ++it) {
49+ if (*it) {
50+ (*it)();
51+ }
52+ }
53+ m_destructionCallbacks.clear ();
54+
4255 for (const auto &[name, cate] : m_moduleCategories) {
4356 (void ) name;
4457 delete cate;
@@ -60,6 +73,10 @@ namespace srt::core {
6073
6174 Runtime::~Runtime () = default ;
6275
76+ void Runtime::addDestructionCallback (std::function<void ()> callback) {
77+ _impl->m_destructionCallbacks .push_back (std::move (callback));
78+ }
79+
6380 // --- Service registry (ARCH-03) ---
6481
6582 ServiceRegistry &Runtime::services () {
Original file line number Diff line number Diff line change 22
33#include < atomic>
44#include < filesystem>
5+ #include < functional>
56#include < map>
67#include < memory>
78#include < shared_mutex>
@@ -75,6 +76,24 @@ namespace srt::core {
7576 // Shared mutex protecting category/contribute operations across packages.
7677 mutable std::shared_mutex m_su_mtx;
7778
79+ // --- Destruction callbacks (DLL unload ordering) ---
80+ // Runtime is typically a local variable in the host (e.g. lite's
81+ // SynthrtEngine::m_runtime), while some objects that reference Runtime
82+ // resources (e.g. srt::g2p::Manager singleton holding Task/SessionFactory
83+ // shared_ptrs that reference plugin DLL code) are static singletons
84+ // whose destructors run AFTER the Runtime. When the Runtime destructs,
85+ // PluginFactory unloads plugin DLLs (e.g. srt-driver-onnx.dll); the
86+ // static singleton's later destructor would then access freed memory
87+ // through dangling vtable pointers.
88+ //
89+ // Destruction callbacks run in ~Runtime::Impl (BEFORE PluginFactory
90+ // unloads DLLs) so subsystems can release object references while the
91+ // plugin DLLs are still loaded. Callbacks execute in reverse order of
92+ // registration (LIFO), and are cleared after execution to break
93+ // reference cycles (captured shared_ptrs would otherwise keep the
94+ // callback itself alive).
95+ std::vector<std::function<void ()>> m_destructionCallbacks;
96+
7897 public:
7998 static llvm::SmallVector<ModuleCategory *(*)(Runtime *)> moduleCategoryFactories;
8099 };
Original file line number Diff line number Diff line change @@ -246,6 +246,16 @@ namespace srt::g2p {
246246 }
247247
248248 PackageManager::Impl::~Impl () {
249+ shutdown ();
250+ }
251+
252+ void PackageManager::Impl::shutdown () {
253+ // Idempotent: safe to call from both shutdown() and the destructor.
254+ // Also safe to call when m_categories is already empty (no-op).
255+ if (m_categories.empty () && m_tasks.empty ()) {
256+ return ;
257+ }
258+
249259 // Destruction order (matters!):
250260 //
251261 // 1. m_tasks.clear() — release the NO<Task> references held by the
@@ -614,6 +624,11 @@ namespace srt::g2p {
614624
615625 PackageManager::~PackageManager () = default ;
616626
627+ void PackageManager::shutdown () {
628+ auto &impl = *static_cast <Impl *>(_impl.get ());
629+ impl.shutdown ();
630+ }
631+
617632 srt::core::ModuleCategory *PackageManager::category (const std::string_view &name) const {
618633 auto &impl = *static_cast <Impl *>(_impl.get ());
619634 const auto it = impl.m_categories .find (name);
Original file line number Diff line number Diff line change @@ -52,6 +52,13 @@ namespace srt::g2p {
5252 void closeAllLoadedPackages ();
5353 void refreshPackageIndexes (const srt::core::ContextKey &ctxKey);
5454
55+ // / Release all object references held by category ObjectPools, close
56+ // / loaded packages, and delete categories. Idempotent (safe to call
57+ // / again from the destructor). Called by PackageManager::shutdown()
58+ // / which is invoked from the Runtime destruction callback registered
59+ // / in setupG2pOnnxDriver.
60+ void shutdown ();
61+
5562 // / Erase all per-context state for \p ctxKey (TD-02). Centralizes the
5663 // / 7-map erase previously duplicated in removeContextsByPrefix
5764 // / overloads. New per-context state should be added here so retire
Original file line number Diff line number Diff line change @@ -223,6 +223,26 @@ namespace srt::g2p {
223223 driverCat->removeObjects (srt::g2p::kG2pOnnxDriverName );
224224 driverCat->addObject (srt::g2p::kG2pOnnxDriverName , factory);
225225
226+ // 5. Register a Runtime destruction callback to call
227+ // Manager::shutdown() BEFORE the Runtime (and its
228+ // PluginFactory) destructs. This releases Task/SessionFactory
229+ // shared_ptrs held by the Manager's category ObjectPools while
230+ // plugin DLLs (e.g. srt-driver-onnx.dll) are still loaded.
231+ //
232+ // Without this, the Manager (a static singleton) would destruct
233+ // AFTER the Runtime (a local variable in the host), releasing
234+ // those shared_ptrs and invoking virtual destructors whose
235+ // vtables reside in already-unloaded plugin DLLs — a
236+ // use-after-free that crashes in shared_ptr::_Decref.
237+ //
238+ // The callback captures `mgr` as a raw pointer (the Manager is
239+ // a static singleton whose address is stable throughout the
240+ // process lifetime, so raw-pointer capture is safe). shutdown()
241+ // is idempotent, so a later destructor call is a no-op.
242+ runtime.addDestructionCallback ([mgr]() {
243+ mgr->shutdown ();
244+ });
245+
226246 return srt::core::Expected<void >{};
227247 } catch (const std::exception &e) {
228248 // CODING-02 / ROBUST-02: do not let exceptions cross the public
You can’t perform that action at this time.
0 commit comments