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,54 @@ return NULL; } } + +// For lldb::SBPlatformLocateModuleCallback +%typemap(in) (lldb::SBPlatformLocateModuleCallback 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 != 3) { + PyErr_SetString(PyExc_TypeError, "Expected 3 argument callable object"); + SWIG_fail; + } + + // NOTE: When this is called multiple times, this will leak the Python + // callable object as other callbacks, because this does not call Py_DECREF + // the object. But it should be almost zero impact since this method is + // expected to be called only once. + + // Don't lose the callback reference + Py_INCREF($input); + + $1 = LLDBSwigPythonCallLocateModuleCallback; + $2 = $input; + } +} + +%typemap(typecheck) (lldb::SBPlatformLocateModuleCallback 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,54 @@ SWIG_PYTHON_THREAD_END_BLOCK; } } + +static SBError LLDBSwigPythonCallLocateModuleCallback( + void *callback_baton, const SBModuleSpec &module_spec_sb, + SBFileSpec &module_file_spec_sb, SBFileSpec &symbol_file_spec_sb) { + SWIG_Python_Thread_Block swig_thread_block; + + PyErr_Cleaner py_err_cleaner(true); + PythonObject module_spec_arg = SWIGBridge::ToSWIGWrapper( + std::move(std::make_unique(module_spec_sb))); + PythonObject module_file_spec_arg = SWIGBridge::ToSWIGWrapper( + std::move(std::make_unique(module_file_spec_sb))); + PythonObject symbol_file_spec_arg = SWIGBridge::ToSWIGWrapper( + std::move(std::make_unique(symbol_file_spec_sb))); + + PythonCallable callable = + Retain(reinterpret_cast(callback_baton)); + if (!callable.IsValid()) { + return SBError("The callback callable is not valid."); + } + + PythonObject result = callable(module_spec_arg, module_file_spec_arg, + symbol_file_spec_arg); + + if (!result.IsAllocated()) + return SBError("No result."); + lldb::SBError *sb_error_ptr = nullptr; + if (SWIG_ConvertPtr(result.get(), (void **)&sb_error_ptr, + SWIGTYPE_p_lldb__SBError, 0) == -1) { + return SBError("Result is not SBError."); + } + + if (sb_error_ptr->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 SBError("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 SBError("symbol_file_spec is not SBFileSpec."); + + module_file_spec_sb = *sb_module_file_spec_ptr; + symbol_file_spec_sb = *sb_symbol_file_spec_ptr; + } + + return *sb_error_ptr; +} %} 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 @@ -126,6 +126,10 @@ typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id, void *baton); +typedef SBError (*SBPlatformLocateModuleCallback)( + void *baton, const SBModuleSpec &module_spec, SBFileSpec &module_file_spec, + SBFileSpec &symbol_file_spec); + typedef void *ScriptedObject; } diff --git a/lldb/include/lldb/API/SBPlatform.h b/lldb/include/lldb/API/SBPlatform.h --- a/lldb/include/lldb/API/SBPlatform.h +++ b/lldb/include/lldb/API/SBPlatform.h @@ -169,6 +169,18 @@ /// environment. SBEnvironment GetEnvironment(); + /// Set a callback as an implementation for locating 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. + SBError SetLocateModuleCallback(lldb::SBPlatformLocateModuleCallback callback, + void *callback_baton); + protected: friend class SBDebugger; friend class SBTarget; diff --git a/lldb/source/API/SBPlatform.cpp b/lldb/source/API/SBPlatform.cpp --- a/lldb/source/API/SBPlatform.cpp +++ b/lldb/source/API/SBPlatform.cpp @@ -11,6 +11,7 @@ #include "lldb/API/SBError.h" #include "lldb/API/SBFileSpec.h" #include "lldb/API/SBLaunchInfo.h" +#include "lldb/API/SBModuleSpec.h" #include "lldb/API/SBPlatform.h" #include "lldb/API/SBUnixSignals.h" #include "lldb/Host/File.h" @@ -655,3 +656,41 @@ return SBEnvironment(); } + +SBError SBPlatform::SetLocateModuleCallback( + lldb::SBPlatformLocateModuleCallback callback, void *callback_baton) { + LLDB_INSTRUMENT_VA(this, callback, callback_baton); + PlatformSP platform_sp(GetSP()); + if (!platform_sp) + return SBError("invalid platform"); + + if (!callback) { + // Clear the callback. + platform_sp->SetLocateModuleCallback(nullptr); + return SBError(); + } + + // Platform.h does not accept lldb::SBPlatformLocateModuleCallback directly + // because of the SBModuleSpec and SBFileSpec dependencies. Use a lambda to + // convert ModuleSpec/FileSpec <--> SBModuleSpec/SBFileSpec for the callback + // arguments. + platform_sp->SetLocateModuleCallback( + [callback, callback_baton](const ModuleSpec &module_spec, + FileSpec &module_file_spec, + FileSpec &symbol_file_spec) { + SBModuleSpec module_spec_sb(module_spec); + SBFileSpec module_file_spec_sb; + SBFileSpec symbol_file_spec_sb; + + SBError error = callback(callback_baton, module_spec_sb, + module_file_spec_sb, symbol_file_spec_sb); + + if (error.Success()) { + module_file_spec = module_file_spec_sb.ref(); + symbol_file_spec = symbol_file_spec_sb.ref(); + } + + return error.ref(); + }); + return SBError(); +} diff --git a/lldb/test/API/python_api/sbplatform/TestLocateModuleCallback.py b/lldb/test/API/python_api/sbplatform/TestLocateModuleCallback.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/python_api/sbplatform/TestLocateModuleCallback.py @@ -0,0 +1,338 @@ +""" +Test platform locate 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 LocateModuleCallbackTestCase(TestBase): + def setUp(self): + TestBase.setUp(self) + self.platform = self.dbg.GetSelectedPlatform() + 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.platform.SetLocateModuleCallback(non_callable) + + def test_set_wrong_args(self): + # The callback should accept 3 argument. + def test_args2(a, b): + pass + + with self.assertRaises(TypeError, msg="Expected 3 argument callable object"): + self.platform.SetLocateModuleCallback(test_args2) + + def test_default(self): + # The default behavior is to locate the module with LLDB implementation + # and AddModule should fail. + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_set_none(self): + # SetLocateModuleCallback should succeed to clear the callback with None. + # and AddModule should fail. + self.assertTrue(self.platform.SetLocateModuleCallback(None).Success()) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_error(self): + # The callback fails, AddModule should fail. + def test_locate_module( + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + return lldb.SBError("locate module callback failed") + + self.assertTrue( + self.platform.SetLocateModuleCallback(test_locate_module).Success() + ) + + module = self.target.AddModule( + MODULE_PLATFORM_PATH, + MODULE_TRIPLE, + MODULE_UUID, + ) + + self.assertFalse(module) + + def test_return_no_files(self): + # The callback succeeds but not return any files, AddModule should fail. + def test_locate_module( + module_spec: lldb.SBModuleSpec, + module_file_spec: lldb.SBFileSpec, + symbol_file_spec: lldb.SBFileSpec, + ): + self.check_module_spec(module_spec) + return lldb.SBError() + + self.assertTrue( + self.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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_locate_module( + 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.platform.SetLocateModuleCallback(test_locate_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, + )