Skip to content

Commit 9f62775

Browse files
authored
SBDebugger: Add new APIs AddDestroyCallback and RemoveDestroyCallback (#89868)
# Motivation Individual callers of `SBDebugger::SetDestroyCallback()` might think that they have registered their callback and expect it to be called when the debugger is destroyed. In reality, only the last caller survives, and all previous callers are forgotten, which might be a surprise to them. Worse, if this is called in a race condition, which callback survives is less predictable, which may case confusing behavior elsewhere. # This PR Allows multiple destroy callbacks to be registered and all called when the debugger is destroyed. **EDIT**: Adds two new APIs: `AddDestroyCallback()` and `ClearDestroyCallback()`. `SetDestroyCallback()` will first clear then add the given callback. Tests are added for the new APIs. ## Tests ``` bin/llvm-lit -sv ../external/llvm-project/lldb/test/API/python_api/debugger/TestDebuggerAPI.py ``` ## (out-dated, see comments below) Semantic change to `SetDestroyCallback()` ~~Currently, the method overwrites the old callback with the new one. With this PR, it will NOT overwrite. Instead, it will hold on to both. Both callbacks get called during destroy.~~ ~~**Risk**: Although the documentation of `SetDestroyCallback()` (see [C++](https://lldb.llvm.org/cpp_reference/classlldb_1_1SBDebugger.html#afa1649d9453a376b5c95888b5a0cb4ec) and [python](https://lldb.llvm.org/python_api/lldb.SBDebugger.html#lldb.SBDebugger.SetDestroyCallback)) doesn't really specify the behavior, there is a risk: if existing call sites rely on the "overwrite" behavior, they will be surprised because now the old callback will get called. But as the above said, the current behavior of "overwrite" itself might be unintended, so I don't anticipate users to rely on this behavior. In short, this risk might be less of a problem if we correct it sooner rather than later (which is what this PR is trying to do).~~ ## (out-dated, see comments below) Implementation ~~The implementation holds a `std::vector<std::pair<callback, baton>>`. When `SetDestroyCallback()` is called, callbacks and batons are appended to the `std::vector`. When destroy event happen, the `(callback, baton)` pairs are invoked FIFO. Finally, the `std::vector` is cleared.~~ # (out-dated, see comments below) Alternatives considered ~~Instead of changing `SetDestroyCallback()`, a new method `AddDestroyCallback()` can be added, which use the same `std::vector<std::pair<>>` implementation. Together with `ClearDestroyCallback()` (see below), they will replace and deprecate `SetDestroyCallback()`. Meanwhile, in order to be backward compatible, `SetDestroyCallback()` need to be updated to clear the `std::vector` and then add the new callback. Pros: The end state is semantically more correct. Cons: More steps to take; potentially maintaining an "incorrect" behavior (of "overwrite").~~ ~~A new method `ClearDestroyCallback()` can be added. Might be unnecessary at this point, because workflows which need to set then clear callbacks may exist but shouldn't be too common at least for now. Such method can be added later when needed.~~ ~~The `std::vector` may bring slight performance drawback if its implementation doesn't handle small size efficiently. However, even if that's the case, this path should be very cold (only used during init and destroy). Such performance drawback should be negligible.~~ ~~A different implementation was also considered. Instead of using `std::vector`, the current `m_destroy_callback` field can be kept unchanged. When `SetDestroyCallback()` is called, a lambda function can be stored into `m_destroy_callback`. This lambda function will first call the old callback, then the new one. This way, `std::vector` is avoided. However, this implementation is more complex, thus less readable, with not much perf to gain.~~ --------- Co-authored-by: Roy Shi <[email protected]>
1 parent e8dc8d6 commit 9f62775

File tree

6 files changed

+225
-7
lines changed

6 files changed

+225
-7
lines changed

lldb/include/lldb/API/SBDebugger.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,22 @@ class LLDB_API SBDebugger {
328328

329329
void SetLoggingCallback(lldb::LogOutputCallback log_callback, void *baton);
330330

331+
/// Clear all previously added callbacks and only add the given one.
332+
LLDB_DEPRECATED_FIXME("Use AddDestroyCallback and RemoveDestroyCallback",
333+
"AddDestroyCallback")
331334
void SetDestroyCallback(lldb::SBDebuggerDestroyCallback destroy_callback,
332335
void *baton);
333336

337+
/// Add a callback for when the debugger is destroyed. Return a token, which
338+
/// can be used to remove said callback. Multiple callbacks can be added by
339+
/// calling this function multiple times, and will be invoked in FIFO order.
340+
lldb::callback_token_t
341+
AddDestroyCallback(lldb::SBDebuggerDestroyCallback destroy_callback,
342+
void *baton);
343+
344+
/// Remove the specified callback. Return true if successful.
345+
bool RemoveDestroyCallback(lldb::callback_token_t token);
346+
334347
#ifndef SWIG
335348
LLDB_DEPRECATED_FIXME("Use DispatchInput(const void *, size_t)",
336349
"DispatchInput(const void *, size_t)")

lldb/include/lldb/Core/Debugger.h

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include "lldb/lldb-types.h"
4141

4242
#include "llvm/ADT/ArrayRef.h"
43+
#include "llvm/ADT/SmallVector.h"
4344
#include "llvm/ADT/StringMap.h"
4445
#include "llvm/ADT/StringRef.h"
4546
#include "llvm/Support/DynamicLibrary.h"
@@ -559,10 +560,25 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
559560

560561
static void ReportSymbolChange(const ModuleSpec &module_spec);
561562

563+
/// DEPRECATED: We used to only support one Destroy callback. Now that we
564+
/// support Add and Remove, you should only remove callbacks that you added.
565+
/// Use Add and Remove instead.
566+
///
567+
/// Clear all previously added callbacks and only add the given one.
562568
void
563569
SetDestroyCallback(lldb_private::DebuggerDestroyCallback destroy_callback,
564570
void *baton);
565571

572+
/// Add a callback for when the debugger is destroyed. Return a token, which
573+
/// can be used to remove said callback. Multiple callbacks can be added by
574+
/// calling this function multiple times, and will be invoked in FIFO order.
575+
lldb::callback_token_t
576+
AddDestroyCallback(lldb_private::DebuggerDestroyCallback destroy_callback,
577+
void *baton);
578+
579+
/// Remove the specified callback. Return true if successful.
580+
bool RemoveDestroyCallback(lldb::callback_token_t token);
581+
566582
/// Manually start the global event handler thread. It is useful to plugins
567583
/// that directly use the \a lldb_private namespace and want to use the
568584
/// debugger's default event handler thread instead of defining their own.
@@ -721,8 +737,19 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
721737
lldb::TargetSP m_dummy_target_sp;
722738
Diagnostics::CallbackID m_diagnostics_callback_id;
723739

724-
lldb_private::DebuggerDestroyCallback m_destroy_callback = nullptr;
725-
void *m_destroy_callback_baton = nullptr;
740+
std::mutex m_destroy_callback_mutex;
741+
lldb::callback_token_t m_destroy_callback_next_token = 0;
742+
struct DestroyCallbackInfo {
743+
DestroyCallbackInfo() {}
744+
DestroyCallbackInfo(lldb::callback_token_t token,
745+
lldb_private::DebuggerDestroyCallback callback,
746+
void *baton)
747+
: token(token), callback(callback), baton(baton) {}
748+
lldb::callback_token_t token;
749+
lldb_private::DebuggerDestroyCallback callback;
750+
void *baton;
751+
};
752+
llvm::SmallVector<DestroyCallbackInfo, 2> m_destroy_callbacks;
726753

727754
uint32_t m_interrupt_requested = 0; ///< Tracks interrupt requests
728755
std::mutex m_interrupt_mutex;

lldb/include/lldb/lldb-types.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ typedef void *thread_arg_t; // Host thread argument type
6262
typedef void *thread_result_t; // Host thread result type
6363
typedef void *(*thread_func_t)(void *); // Host thread function type
6464
typedef int pipe_t; // Host pipe type
65+
typedef int callback_token_t;
6566

6667
#endif // _WIN32
6768

6869
#define LLDB_INVALID_PROCESS ((lldb::process_t)-1)
6970
#define LLDB_INVALID_HOST_THREAD ((lldb::thread_t)NULL)
7071
#define LLDB_INVALID_PIPE ((lldb::pipe_t)-1)
72+
#define LLDB_INVALID_CALLBACK_TOKEN ((lldb::callback_token_t) - 1)
7173

7274
typedef void (*LogOutputCallback)(const char *, void *baton);
7375
typedef bool (*CommandOverrideCallback)(void *baton, const char **argv);

lldb/source/API/SBDebugger.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,26 @@ void SBDebugger::SetDestroyCallback(
16951695
}
16961696
}
16971697

1698+
lldb::callback_token_t
1699+
SBDebugger::AddDestroyCallback(lldb::SBDebuggerDestroyCallback destroy_callback,
1700+
void *baton) {
1701+
LLDB_INSTRUMENT_VA(this, destroy_callback, baton);
1702+
1703+
if (m_opaque_sp)
1704+
return m_opaque_sp->AddDestroyCallback(destroy_callback, baton);
1705+
1706+
return LLDB_INVALID_CALLBACK_TOKEN;
1707+
}
1708+
1709+
bool SBDebugger::RemoveDestroyCallback(lldb::callback_token_t token) {
1710+
LLDB_INSTRUMENT_VA(this, token);
1711+
1712+
if (m_opaque_sp)
1713+
return m_opaque_sp->RemoveDestroyCallback(token);
1714+
1715+
return false;
1716+
}
1717+
16981718
SBTrace
16991719
SBDebugger::LoadTraceFromFile(SBError &error,
17001720
const SBFileSpec &trace_description_file) {

lldb/source/Core/Debugger.cpp

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -743,9 +743,22 @@ DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback,
743743
}
744744

745745
void Debugger::HandleDestroyCallback() {
746-
if (m_destroy_callback) {
747-
m_destroy_callback(GetID(), m_destroy_callback_baton);
748-
m_destroy_callback = nullptr;
746+
const lldb::user_id_t user_id = GetID();
747+
// Invoke and remove all the callbacks in an FIFO order. Callbacks which are
748+
// added during this loop will be appended, invoked and then removed last.
749+
// Callbacks which are removed during this loop will not be invoked.
750+
while (true) {
751+
DestroyCallbackInfo callback_info;
752+
{
753+
std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
754+
if (m_destroy_callbacks.empty())
755+
break;
756+
// Pop the first item in the list
757+
callback_info = m_destroy_callbacks.front();
758+
m_destroy_callbacks.erase(m_destroy_callbacks.begin());
759+
}
760+
// Call the destroy callback with user id and baton
761+
callback_info.callback(user_id, callback_info.baton);
749762
}
750763
}
751764

@@ -1427,8 +1440,30 @@ void Debugger::SetLoggingCallback(lldb::LogOutputCallback log_callback,
14271440

14281441
void Debugger::SetDestroyCallback(
14291442
lldb_private::DebuggerDestroyCallback destroy_callback, void *baton) {
1430-
m_destroy_callback = destroy_callback;
1431-
m_destroy_callback_baton = baton;
1443+
std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
1444+
m_destroy_callbacks.clear();
1445+
const lldb::callback_token_t token = m_destroy_callback_next_token++;
1446+
m_destroy_callbacks.emplace_back(token, destroy_callback, baton);
1447+
}
1448+
1449+
lldb::callback_token_t Debugger::AddDestroyCallback(
1450+
lldb_private::DebuggerDestroyCallback destroy_callback, void *baton) {
1451+
std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
1452+
const lldb::callback_token_t token = m_destroy_callback_next_token++;
1453+
m_destroy_callbacks.emplace_back(token, destroy_callback, baton);
1454+
return token;
1455+
}
1456+
1457+
bool Debugger::RemoveDestroyCallback(lldb::callback_token_t token) {
1458+
std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
1459+
for (auto it = m_destroy_callbacks.begin(); it != m_destroy_callbacks.end();
1460+
++it) {
1461+
if (it->token == token) {
1462+
m_destroy_callbacks.erase(it);
1463+
return true;
1464+
}
1465+
}
1466+
return false;
14321467
}
14331468

14341469
static void PrivateReportProgress(Debugger &debugger, uint64_t progress_id,

lldb/test/API/python_api/debugger/TestDebuggerAPI.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,124 @@ def foo(dbg_id):
161161
original_dbg_id = self.dbg.GetID()
162162
self.dbg.Destroy(self.dbg)
163163
self.assertEqual(destroy_dbg_id, original_dbg_id)
164+
165+
def test_AddDestroyCallback(self):
166+
original_dbg_id = self.dbg.GetID()
167+
called = []
168+
169+
def foo(dbg_id):
170+
# Need nonlocal to modify closure variable.
171+
nonlocal called
172+
called += [('foo', dbg_id)]
173+
174+
def bar(dbg_id):
175+
# Need nonlocal to modify closure variable.
176+
nonlocal called
177+
called += [('bar', dbg_id)]
178+
179+
token_foo = self.dbg.AddDestroyCallback(foo)
180+
token_bar = self.dbg.AddDestroyCallback(bar)
181+
self.dbg.Destroy(self.dbg)
182+
183+
# Should call both `foo()` and `bar()`.
184+
self.assertEqual(called, [
185+
('foo', original_dbg_id),
186+
('bar', original_dbg_id),
187+
])
188+
189+
def test_RemoveDestroyCallback(self):
190+
original_dbg_id = self.dbg.GetID()
191+
called = []
192+
193+
def foo(dbg_id):
194+
# Need nonlocal to modify closure variable.
195+
nonlocal called
196+
called += [('foo', dbg_id)]
197+
198+
def bar(dbg_id):
199+
# Need nonlocal to modify closure variable.
200+
nonlocal called
201+
called += [('bar', dbg_id)]
202+
203+
token_foo = self.dbg.AddDestroyCallback(foo)
204+
token_bar = self.dbg.AddDestroyCallback(bar)
205+
ret = self.dbg.RemoveDestroyCallback(token_foo)
206+
self.dbg.Destroy(self.dbg)
207+
208+
# `Remove` should be successful
209+
self.assertTrue(ret)
210+
# Should only call `bar()`
211+
self.assertEqual(called, [('bar', original_dbg_id)])
212+
213+
def test_RemoveDestroyCallback_invalid_token(self):
214+
original_dbg_id = self.dbg.GetID()
215+
magic_token_that_should_not_exist = 32413
216+
called = []
217+
218+
def foo(dbg_id):
219+
# Need nonlocal to modify closure variable.
220+
nonlocal called
221+
called += [('foo', dbg_id)]
222+
223+
token_foo = self.dbg.AddDestroyCallback(foo)
224+
ret = self.dbg.RemoveDestroyCallback(magic_token_that_should_not_exist)
225+
self.dbg.Destroy(self.dbg)
226+
227+
# `Remove` should be unsuccessful
228+
self.assertFalse(ret)
229+
# Should call `foo()`
230+
self.assertEqual(called, [('foo', original_dbg_id)])
231+
232+
def test_HandleDestroyCallback(self):
233+
"""
234+
Validates:
235+
1. AddDestroyCallback and RemoveDestroyCallback work during debugger destroy.
236+
2. HandleDestroyCallback invokes all callbacks in FIFO order.
237+
"""
238+
original_dbg_id = self.dbg.GetID()
239+
events = []
240+
bar_token = None
241+
242+
def foo(dbg_id):
243+
# Need nonlocal to modify closure variable.
244+
nonlocal events
245+
events.append(('foo called', dbg_id))
246+
247+
def bar(dbg_id):
248+
# Need nonlocal to modify closure variable.
249+
nonlocal events
250+
events.append(('bar called', dbg_id))
251+
252+
def add_foo(dbg_id):
253+
# Need nonlocal to modify closure variable.
254+
nonlocal events
255+
events.append(('add_foo called', dbg_id))
256+
events.append(('foo token', self.dbg.AddDestroyCallback(foo)))
257+
258+
def remove_bar(dbg_id):
259+
# Need nonlocal to modify closure variable.
260+
nonlocal events
261+
events.append(('remove_bar called', dbg_id))
262+
events.append(('remove bar ret', self.dbg.RemoveDestroyCallback(bar_token)))
263+
264+
# Setup
265+
events.append(('add_foo token', self.dbg.AddDestroyCallback(add_foo)))
266+
bar_token = self.dbg.AddDestroyCallback(bar)
267+
events.append(('bar token', bar_token))
268+
events.append(('remove_bar token', self.dbg.AddDestroyCallback(remove_bar)))
269+
# Destroy
270+
self.dbg.Destroy(self.dbg)
271+
272+
self.assertEqual(events, [
273+
# Setup
274+
('add_foo token', 0), # add_foo should be added
275+
('bar token', 1), # bar should be added
276+
('remove_bar token', 2), # remove_bar should be added
277+
# Destroy
278+
('add_foo called', original_dbg_id), # add_foo should be called
279+
('foo token', 3), # foo should be added
280+
('bar called', original_dbg_id), # bar should be called
281+
('remove_bar called', original_dbg_id), # remove_bar should be called
282+
('remove bar ret', False), # remove_bar should fail, because it's already invoked and removed
283+
('foo called', original_dbg_id), # foo should be called
284+
])

0 commit comments

Comments
 (0)