diff --git a/lldb/bindings/python/python-typemaps.swig b/lldb/bindings/python/python-typemaps.swig --- a/lldb/bindings/python/python-typemaps.swig +++ b/lldb/bindings/python/python-typemaps.swig @@ -576,3 +576,57 @@ return NULL; } } + +// For lldb::SBTargetGetModuleCallback +%typemap(in) (lldb::SBTargetGetModuleCallback callback, + void *callback_baton) { + if (!($input == Py_None || + PyCallable_Check(reinterpret_cast($input)))) { + PyErr_SetString(PyExc_TypeError, "Need a callable object or None!"); + SWIG_fail; + } + + if ($input == Py_None) { + $1 = nullptr; + $2 = nullptr; + } else { + PythonCallable callable = Retain($input); + if (!callable.IsValid()) { + PyErr_SetString(PyExc_TypeError, "Need a valid callable object"); + SWIG_fail; + } + + llvm::Expected arg_info = callable.GetArgInfo(); + if (!arg_info) { + PyErr_SetString(PyExc_TypeError, + ("Could not get arguments: " + + llvm::toString(arg_info.takeError())).c_str()); + SWIG_fail; + } + + if (arg_info.get().max_positional_args != 4) { + PyErr_SetString(PyExc_TypeError, "Expected 4 argument callable object"); + SWIG_fail; + } + + // Don't lose the callback reference + Py_INCREF($input); + + // lldb::SBTarget::SetGetModuleCallback uses lldb::SBTargetGetModuleCallback + // type for the Python interface, however lldb_private::Target class does + // not use it. This is because we do not want to add the SB* dependencies to + // lldb_private::Target. The actual Python callable object is turned into + // the second argument as 'void *callback_baton' of + // lldb::SBTarget::SetGetModuleCallback by this conversion. Therefore + // lldb_private::Target class can retain the callable as 'void *' without + // the SB* or Python dependencies. + $1 = nullptr; + $2 = $input; + } +} + +%typemap(typecheck) (lldb::SBTargetGetModuleCallback callback, + void *callback_baton) { + $1 = $input == Py_None; + $1 = $1 || PyCallable_Check(reinterpret_cast($input)); +} diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig --- a/lldb/bindings/python/python-wrapper.swig +++ b/lldb/bindings/python/python-wrapper.swig @@ -1118,4 +1118,64 @@ SWIG_PYTHON_THREAD_END_BLOCK; } } + +Status lldb_private::python::SWIGBridge::LLDBSWIGPythonCallTargetGetModuleCallback( + void *callback_baton, lldb::DebuggerSP debugger_sp, + const ModuleSpec &module_spec, FileSpec &module_file_spec, + FileSpec &symbol_file_spec) { + // Not possible to use std::make_unique because of the private constructor. + std::unique_ptr + module_spec_sb(new lldb::SBModuleSpec(module_spec)); + auto module_file_spec_sb = std::make_unique(); + auto symbol_file_spec_sb = std::make_unique(); + + PythonObject debugger_arg = SWIGBridge::ToSWIGWrapper(std::move(debugger_sp)); + PythonObject module_spec_arg = + SWIGBridge::ToSWIGWrapper(std::move(module_spec_sb)); + PythonObject module_file_spec_arg = + SWIGBridge::ToSWIGWrapper(std::move(module_file_spec_sb)); + PythonObject symbol_file_spec_arg = + SWIGBridge::ToSWIGWrapper(std::move(symbol_file_spec_sb)); + + PyErr_Cleaner py_err_cleaner(true); + + PythonCallable callable = + Retain(reinterpret_cast(callback_baton)); + if (!callable.IsValid()) { + return Status("The callback callable is not valid."); + } + + PythonObject result = callable(debugger_arg, module_spec_arg, + module_file_spec_arg, symbol_file_spec_arg); + + if (!result.IsAllocated()) + return Status("No result."); + lldb::SBError *sb_error_ptr = nullptr; + if (SWIG_ConvertPtr(result.get(), (void **)&sb_error_ptr, + SWIGTYPE_p_lldb__SBError, 0) == -1) { + return Status("Result is not SBError."); + } + Status error = sb_error_ptr->ref(); + if (error.Success()) { + lldb::SBFileSpec *sb_module_file_spec_ptr = nullptr; + if (SWIG_ConvertPtr(module_file_spec_arg.get(), + (void **)&sb_module_file_spec_ptr, + SWIGTYPE_p_lldb__SBFileSpec, 0) == -1) + return Status("module_file_spec is not SBFileSpec."); + + lldb::SBFileSpec *sb_symbol_file_spec_ptr = nullptr; + if (SWIG_ConvertPtr(symbol_file_spec_arg.get(), + (void **)&sb_symbol_file_spec_ptr, + SWIGTYPE_p_lldb__SBFileSpec, 0) == -1) + return Status("symbol_file_spec is not SBFileSpec."); + + module_file_spec.SetDirectory(sb_module_file_spec_ptr->GetDirectory()); + module_file_spec.SetFilename(sb_module_file_spec_ptr->GetFilename()); + + symbol_file_spec.SetDirectory(sb_symbol_file_spec_ptr->GetDirectory()); + symbol_file_spec.SetFilename(sb_symbol_file_spec_ptr->GetFilename()); + } + + return error; +} %} diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h --- a/lldb/include/lldb/API/SBDebugger.h +++ b/lldb/include/lldb/API/SBDebugger.h @@ -467,6 +467,20 @@ SBTrace LoadTraceFromFile(SBError &error, const SBFileSpec &trace_description_file); + /// Set a callback as an implementation for getting module in order to + /// implement own module cache system. For example, to leverage distributed + /// build system, to bypass pulling files from remote platform, or to search + /// symbol files from symbol servers. The target will call this callback to + /// get a module file and a symbol file, and it will fallback to the LLDB + /// implementation when this callback failed or returned non-existent file. + /// This callback can set either module_file_spec or symbol_file_spec, or both + /// module_file_spec and symbol_file_spec. The callback will be cleared if + /// nullptr or None is set. + // Why this exists in SBDebugger instance and not in SBTarget instance because + // this needs to be set before creating Target instance. + SBError SetTargetGetModuleCallback(lldb::SBTargetGetModuleCallback callback, + void *callback_baton); + protected: friend class lldb_private::CommandPluginInterfaceImplementation; friend class lldb_private::python::SWIGBridge; diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h --- a/lldb/include/lldb/API/SBDefines.h +++ b/lldb/include/lldb/API/SBDefines.h @@ -124,6 +124,11 @@ typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id, void *baton); +typedef SBError (*SBTargetGetModuleCallback)(SBDebugger debugger, + SBModuleSpec &module_spec, + SBFileSpec &module_file_spec, + SBFileSpec &symbol_file_spec); + typedef void *ScriptedObject; } diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp --- a/lldb/source/API/SBDebugger.cpp +++ b/lldb/source/API/SBDebugger.cpp @@ -1722,3 +1722,18 @@ return m_opaque_sp->InterruptRequested(); return false; } + +SBError SBDebugger::SetTargetGetModuleCallback( + lldb::SBTargetGetModuleCallback callback, void *callback_baton) { + LLDB_INSTRUMENT_VA(this, callback, callback_baton); + if (m_opaque_sp) { + // This does not pass the first 'callback' argument to the Debugger + // instance, because it is always nullptr that is passed by + // python-typemaps.swig, in order to not add the SB* dependencies to + // Debugger. 'callback_baton' is the actual callback Python callable object. + m_opaque_sp->SetTargetGetModuleCallback(callback_baton); + return SBError(); + } else { + return SBError("invalid debugger"); + } +} diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h --- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h @@ -261,6 +261,11 @@ static void * LLDBSWIGPython_GetDynamicSetting(void *module, const char *setting, const lldb::TargetSP &target_sp); + + static Status LLDBSWIGPythonCallTargetGetModuleCallback( + void *callback_baton, lldb::DebuggerSP debugger_sp, + const ModuleSpec &module_spec, FileSpec &module_file_spec, + FileSpec &symbol_file_spec); }; void *LLDBSWIGPython_CastPyObjectToSBData(PyObject *data); diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp @@ -1270,6 +1270,16 @@ } } +Status ScriptInterpreterPythonImpl::CallTargetGetModuleCallback( + void *callback_baton, const ModuleSpec &module_spec, + FileSpec &module_file_spec, FileSpec &symbol_file_spec) { + Locker py_lock(this, + Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN); + return SWIGBridge::LLDBSWIGPythonCallTargetGetModuleCallback( + callback_baton, Debugger::FindDebuggerWithID(m_debugger.GetID()), + module_spec, module_file_spec, symbol_file_spec); +} + Status ScriptInterpreterPythonImpl::ExportFunctionDefinitionToInterpreter( StringList &function_def) { // Convert StringList to one long, newline delimited, const char *. diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h @@ -289,6 +289,11 @@ const char *user_input, bool is_callback) override; + Status CallTargetGetModuleCallback(void *callback_baton, + const ModuleSpec &module_spec, + FileSpec &module_file_spec, + FileSpec &symbol_file_spec) override; + const char *GetDictionaryName() { return m_dictionary_name.c_str(); } PyThreadState *GetThreadState() { return m_command_thread_state; } diff --git a/lldb/test/API/python_api/target/TestGetModuleCallback.py b/lldb/test/API/python_api/target/TestGetModuleCallback.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/python_api/target/TestGetModuleCallback.py @@ -0,0 +1,288 @@ +""" +Test Target get module callback functionality +""" + +import ctypes +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from pathlib import Path + +import lldb + +UNITTESTS_TARGET_INPUTS_PATH = "../../../../unittests/Target/Inputs" +MODULE_PLATFORM_PATH = "/system/lib64/AndroidModule.so" +MODULE_TRIPLE = "aarch64-none-linux" +MODULE_RESOLVED_TRIPLE = "aarch64--linux-android" +MODULE_UUID = "80008338-82A0-51E5-5922-C905D23890DA-BDDEFECC" +MODULE_FUNCTION = "boom" +MODULE_HIDDEN_FUNCTION = "boom_hidden" +MODULE_FILE = "AndroidModule.so" +MODULE_NON_EXISTENT_FILE = "non-existent-file" +SYMBOL_FILE = "AndroidModule.unstripped.so" +BREAKPAD_SYMBOL_FILE = "AndroidModule.so.sym" +SYMBOL_STRIPPED = "stripped" +SYMBOL_UNSTRIPPED = "unstripped" + + +class GetModuleCallbackTestCase(TestBase): + def setUp(self): + TestBase.setUp(self) + self.target = self.dbg.CreateTarget("") + self.assertTrue(self.target) + + self.input_dir = ( + Path(self.getSourceDir()) / UNITTESTS_TARGET_INPUTS_PATH + ).resolve() + self.assertTrue(self.input_dir.is_dir()) + + def check_module_spec(self, module_spec: lldb.SBModuleSpec): + self.assertEqual( + MODULE_UUID.replace("-", ""), + ctypes.string_at( + int(module_spec.GetUUIDBytes()), + module_spec.GetUUIDLength(), + ) + .hex() + .upper(), + ) + + self.assertEqual(MODULE_TRIPLE, module_spec.GetTriple()) + + self.assertEqual(MODULE_PLATFORM_PATH, module_spec.GetFileSpec().fullpath) + + def check_module(self, module: lldb.SBModule, symbol_file: str, symbol_kind: str): + self.assertTrue(module.IsValid()) + + self.assertEqual( + MODULE_UUID, + module.GetUUIDString(), + ) + + self.assertEqual(MODULE_RESOLVED_TRIPLE, module.GetTriple()) + + self.assertEqual(MODULE_PLATFORM_PATH, module.GetPlatformFileSpec().fullpath) + + self.assertEqual( + str(self.input_dir / MODULE_FILE), + module.GetFileSpec().fullpath, + ) + + self.assertEqual( + str(self.input_dir / symbol_file), + module.GetSymbolFileSpec().fullpath, + ) + + sc_list = module.FindFunctions(MODULE_FUNCTION, lldb.eSymbolTypeCode) + self.assertEqual(1, sc_list.GetSize()) + sc_list = module.FindFunctions(MODULE_HIDDEN_FUNCTION, lldb.eSymbolTypeCode) + self.assertEqual(0 if symbol_kind == SYMBOL_STRIPPED else 1, sc_list.GetSize()) + + def test_set_non_callable(self): + # The callback should be callable. + non_callable = "a" + + with self.assertRaises(TypeError, msg="Need a callable object or None!"): + self.dbg.SetTargetGetModuleCallback(non_callable) + + def test_set_wrong_args(self): + # The callback should accept 4 argument. + def test_args3(a, b, c): + pass + + with self.assertRaises(TypeError, msg="Expected 4 argument callable object"): + self.dbg.SetTargetGetModuleCallback(test_args3) + + def test_set_none(self): + # SetTargetGetModuleCallback should succeed to clear the callback with None. + self.assertTrue(self.dbg.SetTargetGetModuleCallback(None).Success()) + + def test_return_error(self): + # The callback does not return any files, AddModule should fail. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + return lldb.SBError("get module callback failed") + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_non_existent_module(self): + # The callback returns non-existent module file, AddModule should fail. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + module_file_spec.SetDirectory(str(self.input_dir)) + module_file_spec.SetFilename(MODULE_NON_EXISTENT_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_module_with_non_existent_symbol(self): + # The callback returns a module and non-existent symbol file, + # AddModule should fail. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + module_file_spec.SetDirectory(str(self.input_dir)) + module_file_spec.SetFilename(MODULE_FILE) + + symbol_file_spec.SetDirectory(str(self.input_dir)) + symbol_file_spec.SetFilename(MODULE_NON_EXISTENT_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_non_existent_symbol(self): + # The callback returns non-existent symbol file, AddModule should fail. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + symbol_file_spec.SetDirectory(str(self.input_dir)) + symbol_file_spec.SetFilename(MODULE_NON_EXISTENT_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_module(self): + # The callback returns the module file, AddModule should succeed. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + module_file_spec.SetDirectory(str(self.input_dir)) + module_file_spec.SetFilename(MODULE_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.check_module( + module=module, symbol_file=MODULE_FILE, symbol_kind=SYMBOL_STRIPPED + ) + + def test_return_module_with_symbol(self): + # The callback returns the module file and the symbol file, + # AddModule should succeed. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + module_file_spec.SetDirectory(str(self.input_dir)) + module_file_spec.SetFilename(MODULE_FILE) + + symbol_file_spec.SetDirectory(str(self.input_dir)) + symbol_file_spec.SetFilename(SYMBOL_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.check_module( + module=module, symbol_file=SYMBOL_FILE, symbol_kind=SYMBOL_UNSTRIPPED + ) + + def test_return_module_with_breakpad_symbol(self): + # The callback returns the module file and the breakpad symbol file, + # AddModule should succeed. + def test_get_module( + debugger: lldb.SBDebugger, + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + + module_file_spec.SetDirectory(str(self.input_dir)) + module_file_spec.SetFilename(MODULE_FILE) + + symbol_file_spec.SetDirectory(str(self.input_dir)) + symbol_file_spec.SetFilename(BREAKPAD_SYMBOL_FILE) + + return lldb.SBError() + + self.assertTrue(self.dbg.SetTargetGetModuleCallback(test_get_module).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.check_module( + module=module, + symbol_file=BREAKPAD_SYMBOL_FILE, + symbol_kind=SYMBOL_UNSTRIPPED, + ) diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp --- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp +++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp @@ -321,3 +321,11 @@ lldb_private::python::SWIGBridge::ToSWIGWrapper(lldb::DataExtractorSP) { return python::PythonObject(); } + +Status +lldb_private::python::SWIGBridge::LLDBSWIGPythonCallTargetGetModuleCallback( + void *callback_baton, lldb::DebuggerSP debugger_sp, + const ModuleSpec &module_spec, FileSpec &module_file_spec, + FileSpec &symbol_file_spec) { + return Status(); +}