Skip to content

[lldb][ResolveSourceFileCallback] Implement API, Python interface #120834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

rchamala
Copy link
Contributor

Summary:
RFC https://discourse.llvm.org/t/rfc-python-callback-for-target-get-module/71580

Use SWIG for the resolve source file callback the same as other Python callbacks. TestResolveSourceFileCallback.py verifies the functionalities.

Test Plan:
Added shell tests for validation

./llvm-lit -sv TestResolveSourceFileCallback.py

Differential Revision: https://phabricator.intern.facebook.com/D67541203

Summary:
RFC https://discourse.llvm.org/t/rfc-python-callback-for-target-get-module/71580

Use SWIG for the resolve source file callback the same as other Python callbacks. TestResolveSourceFileCallback.py verifies the functionalities.

Test Plan:
Added shell tests for validation

```
./llvm-lit -sv TestResolveSourceFileCallback.py
```

Differential Revision: https://phabricator.intern.facebook.com/D67541203
Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@rchamala
Copy link
Contributor Author

Will rebase changes from #120832 and #120832 once they are approved which should fix the errors

@rchamala rchamala marked this pull request as ready for review December 21, 2024 09:13
@llvmbot llvmbot added the lldb label Dec 21, 2024
@llvmbot
Copy link
Member

llvmbot commented Dec 21, 2024

@llvm/pr-subscribers-lldb

Author: None (rchamala)

Changes

Summary:
RFC https://discourse.llvm.org/t/rfc-python-callback-for-target-get-module/71580

Use SWIG for the resolve source file callback the same as other Python callbacks. TestResolveSourceFileCallback.py verifies the functionalities.

Test Plan:
Added shell tests for validation

./llvm-lit -sv TestResolveSourceFileCallback.py

Differential Revision: https://phabricator.intern.facebook.com/D67541203


Full diff: https://github.com/llvm/llvm-project/pull/120834.diff

8 Files Affected:

  • (modified) lldb/bindings/python/python-typemaps.swig (+44)
  • (modified) lldb/bindings/python/python-wrapper.swig (+47-2)
  • (modified) lldb/include/lldb/API/SBDefines.h (+5)
  • (modified) lldb/include/lldb/API/SBPlatform.h (+3)
  • (modified) lldb/source/API/SBPlatform.cpp (+37)
  • (added) lldb/test/API/python_api/sbplatform/TestResolveSourceFileCallback.py (+196)
  • (added) lldb/test/API/python_api/sbplatform/test.exe ()
  • (added) lldb/test/API/python_api/sbplatform/test_new.cpp (+15)
diff --git a/lldb/bindings/python/python-typemaps.swig b/lldb/bindings/python/python-typemaps.swig
index f8c33e15c03e66..84d26986104d31 100644
--- a/lldb/bindings/python/python-typemaps.swig
+++ b/lldb/bindings/python/python-typemaps.swig
@@ -713,3 +713,47 @@ template <> bool SetNumberFromPyObject<double>(double &number, PyObject *obj) {
   $1 = $input == Py_None;
   $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
 }
+
+// For lldb::SBPlatformResolveSourceFileCallback
+%typemap(in) (lldb::SBPlatformResolveSourceFileCallback callback, void *callback_baton) {
+  if (!($input == Py_None ||
+       PyCallable_Check(reinterpret_cast<PyObject *>($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<PythonCallable>($input);
+    if (!callable.IsValid()) {
+      PyErr_SetString(PyExc_TypeError, "Need a valid callable object");
+      SWIG_fail;
+    }
+
+  llvm::Expected<PythonCallable::ArgInfo> 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;
+  }
+
+    Py_INCREF($input);
+
+    $1 = LLDBSwigPythonCallResolveSourceFileCallback;
+    $2 = $input;
+  }
+}
+
+%typemap(typecheck) (lldb::SBPlatformResolveSourceFileCallback callback,
+                     void *callback_baton) {
+  $1 = $input == Py_None;
+  $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
+}
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index b72a462d04643b..fb0b0368914fbf 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -727,7 +727,7 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionFo
     dict_sp->AddBooleanItem("no-completion", true);
     return dict_sp;
   }
-    
+
 
   // Convert the return dictionary to a DictionarySP.
   StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
@@ -753,7 +753,7 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
   auto pfunc = self.ResolveName<PythonCallable>("__call__");
 
   if (!pfunc.IsAllocated()) {
-    cmd_retobj.AppendError("Could not find '__call__' method in implementation class"); 
+    cmd_retobj.AppendError("Could not find '__call__' method in implementation class");
     return false;
   }
 
@@ -1095,4 +1095,49 @@ static SBError LLDBSwigPythonCallLocateModuleCallback(
 
   return *sb_error_ptr;
 }
+
+static SBError LLDBSwigPythonCallResolveSourceFileCallback(
+    void *callback_baton,
+     const lldb::ModuleSP &module_sp,
+    const SBFileSpec &original_source_file_spec_sb,
+    SBFileSpec &resolved_source_file_spec_sb) {
+  SWIG_Python_Thread_Block swig_thread_block;
+
+  PyErr_Cleaner py_err_cleaner(true);
+
+  PythonObject module_sb_arg = SWIGBridge::ToSWIGWrapper(module_sp);
+  PythonObject original_source_file_spec_arg = SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBFileSpec>(original_source_file_spec_sb));
+  PythonObject resolved_source_file_spec_arg = SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBFileSpec>(resolved_source_file_spec_sb));
+
+  PythonCallable callable =
+      Retain<PythonCallable>(reinterpret_cast<PyObject *>(callback_baton));
+  if (!callable.IsValid()) {
+    return SBError("The callback callable is not valid.");
+  }
+
+  PythonObject result = callable(module_sb_arg, original_source_file_spec_arg,
+                                 resolved_source_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_resolved_source_file_spec_ptr = nullptr;
+    if (SWIG_ConvertPtr(resolved_source_file_spec_arg.get(),
+                        (void **)&sb_resolved_source_file_spec_ptr,
+                        SWIGTYPE_p_lldb__SBFileSpec, 0) == -1)
+      return SBError("resolved_source_file_spec is not SBFileSpec.");
+
+    resolved_source_file_spec_sb = *sb_resolved_source_file_spec_ptr;
+  }
+
+  return *sb_error_ptr;
+}
 %}
diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h
index 159a9ba799b181..fa676ed799bbeb 100644
--- a/lldb/include/lldb/API/SBDefines.h
+++ b/lldb/include/lldb/API/SBDefines.h
@@ -146,6 +146,11 @@ typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id,
 typedef lldb::SBError (*SBPlatformLocateModuleCallback)(
     void *baton, const lldb::SBModuleSpec &module_spec,
     lldb::SBFileSpec &module_file_spec, lldb::SBFileSpec &symbol_file_spec);
+
+typedef lldb::SBError (*SBPlatformResolveSourceFileCallback)(
+    void *baton, const lldb::ModuleSP &module_sp,
+    const lldb::SBFileSpec &original_source_file_spec,
+    lldb::SBFileSpec &resolved_source_file_spec);
 }
 
 #endif // LLDB_API_SBDEFINES_H
diff --git a/lldb/include/lldb/API/SBPlatform.h b/lldb/include/lldb/API/SBPlatform.h
index d63d2ed1eaba62..37ab2ef0441cf5 100644
--- a/lldb/include/lldb/API/SBPlatform.h
+++ b/lldb/include/lldb/API/SBPlatform.h
@@ -190,6 +190,9 @@ class LLDB_API SBPlatform {
   SBError SetLocateModuleCallback(lldb::SBPlatformLocateModuleCallback callback,
                                   void *callback_baton);
 
+  SBError SetResolveSourceFileCallback(
+      lldb::SBPlatformResolveSourceFileCallback 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
index 394268b77aa21f..f0fd0a3418fa56 100644
--- a/lldb/source/API/SBPlatform.cpp
+++ b/lldb/source/API/SBPlatform.cpp
@@ -732,3 +732,40 @@ SBError SBPlatform::SetLocateModuleCallback(
       });
   return SBError();
 }
+
+SBError SBPlatform::SetResolveSourceFileCallback(
+    lldb::SBPlatformResolveSourceFileCallback 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->SetResolveSourceFileCallback(nullptr);
+    return SBError();
+  }
+
+  // Platform.h does not accept lldb::SBPlatform ResolveSourceFileCallback
+  // directly because of the SBFileSpec dependency. Use a lambda to
+  // convert FileSpec <--> SBFileSpec for the callback arguments.
+  platform_sp->SetResolveSourceFileCallback(
+      [callback, callback_baton](const lldb::ModuleSP &module_sp,
+                                 const FileSpec &original_source_file_spec,
+                                 FileSpec &resolved_source_file_spec) {
+        SBFileSpec original_source_file_spec_sb(original_source_file_spec);
+        SBFileSpec resolved_source_file_spec_sb;
+
+        SBError error =
+            callback(callback_baton, module_sp, original_source_file_spec_sb,
+                     resolved_source_file_spec_sb);
+
+        if (error.Success()) {
+          resolved_source_file_spec = resolved_source_file_spec_sb.ref();
+        }
+
+        return error.ref().Clone();
+      });
+  return SBError();
+}
diff --git a/lldb/test/API/python_api/sbplatform/TestResolveSourceFileCallback.py b/lldb/test/API/python_api/sbplatform/TestResolveSourceFileCallback.py
new file mode 100644
index 00000000000000..78bf4e47145ca7
--- /dev/null
+++ b/lldb/test/API/python_api/sbplatform/TestResolveSourceFileCallback.py
@@ -0,0 +1,196 @@
+"""
+Test resolve source file callback functionality
+"""
+
+import os
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from pathlib import Path
+
+import lldb
+
+SOURCE_ORIGINAL_FILE = "test.cpp"  # File does not exist
+SOURCE_NEW_FILE = "test_new.cpp"  # File exists
+SOURCE_NEW_NON_EXISTENT_FILE = "non-existent-file"
+EXE_NAME = "test.exe"
+
+
+class ResolveSourceFileCallbackTestCase(TestBase):
+    def setUp(self):
+        TestBase.setUp(self)
+
+        # Set the input directory
+        self.input_dir = (Path(self.getSourceDir())).resolve()
+
+        # Set executable to test.exe and ensure it exists
+        exe_path = (self.input_dir / EXE_NAME).resolve()
+        self.assertTrue(exe_path.exists())
+        exe_path_str = str(exe_path)
+
+        # Create target
+        self.target = self.dbg.CreateTarget(exe_path_str)
+        self.assertTrue(self.target)
+
+        # Create platform
+        self.platform = self.target.GetPlatform()
+
+        # Launch the process once, stop at breakpoint "sum" function and get the frame
+        self.frame = self.get_frame_for_paused_process("sum", exe_path_str)
+
+        # Set the original source file spec
+        source_file_path = os.path.join(self.input_dir, SOURCE_ORIGINAL_FILE)
+        self.original_source_file_spec = lldb.SBFileSpec(source_file_path)
+
+        # Set the new source file spec
+        new_source_file_path = os.path.join(self.input_dir, SOURCE_NEW_FILE)
+        self.new_source_file_spec = lldb.SBFileSpec(new_source_file_path)
+
+    def get_frame_for_paused_process(self, function_name, exe) -> lldb.SBFrame:
+        # Launch the process, stop at breakpoint on function name and get the frame
+
+        # Set breakpoint
+        breakpoint = self.target.BreakpointCreateByName(function_name, exe)
+        self.assertTrue(
+            breakpoint and breakpoint.GetNumLocations() == 1, VALID_BREAKPOINT
+        )
+
+        # Now launch the process, and do not stop at entry point.
+        process = self.target.LaunchSimple(
+            None, None, self.get_process_working_directory()
+        )
+        self.assertTrue(process, PROCESS_IS_VALID)
+
+        # Get the stopped thread
+        thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
+        self.assertTrue(
+            thread.IsValid(), "There should be a thread stopped due to breakpoint"
+        )
+
+        # Get the frame
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertTrue(frame0.IsValid(), "There should be a valid frame")
+
+        return frame0
+
+    def get_source_file_for_frame(self) -> lldb.SBFileSpec:
+        line_entry = self.frame.GetLineEntry()
+        self.assertTrue(line_entry.IsValid(), "There should be a valid line entry")
+
+        return line_entry.GetFileSpec()
+
+    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.SetResolveSourceFileCallback(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.SetResolveSourceFileCallback(test_args2)
+
+    def test_default(self):
+        # The default behavior is to locate the source file with LLDB implementation
+        # and frame.GetLineEntry should return the original file spec.
+        resolved_source_file_spec = self.get_source_file_for_frame()
+
+        # Check if the source file spec is resolved to the original file spec
+        self.assertEqual(resolved_source_file_spec, self.original_source_file_spec)
+        self.assertFalse(self.original_source_file_spec.Exists())
+
+    def test_set_none(self):
+        # SetResolveSourceFileCallback should succeed to clear the callback with None
+        # and frame.GetLineEntry will return the original file spec.
+        self.assertTrue(self.platform.SetResolveSourceFileCallback(None).Success())
+
+        resolved_source_file_spec = self.get_source_file_for_frame()
+
+        # Check if the source file spec is resolved to the original file spec
+        self.assertEqual(resolved_source_file_spec, self.original_source_file_spec)
+        self.assertFalse(resolved_source_file_spec.Exists())
+
+    def test_return_original_file_on_error(self):
+        # The callback fails, frame.GetLineEntry should return the original file spec.
+
+        # Resolve Source File Callback
+        def test_source_file_callback(
+            module_sp: lldb.SBModule,
+            original_file_spec: lldb.SBFileSpec,
+            resolved_file_spec: lldb.SBFileSpec,
+        ):
+            return lldb.SBError("Resolve Source File Callback failed")
+
+        self.assertTrue(
+            self.platform.SetResolveSourceFileCallback(
+                test_source_file_callback
+            ).Success()
+        )
+
+        resolved_source_file_spec = self.get_source_file_for_frame()
+
+        # Check if the source file spec is resolved to the original file spec
+        self.assertEqual(resolved_source_file_spec, self.original_source_file_spec)
+        self.assertFalse(resolved_source_file_spec.Exists())
+
+    def test_return_orignal_file_with_new_nonexistent_file(self):
+        # The callback should return a valid SBFileSpec but the file does not exist.
+        # frame.GetLineEntry should return the original file spec.
+
+        # Resolve Source File Callback
+        def test_source_file_callback(
+            module_sp: lldb.SBModule,
+            original_file_spec: lldb.SBFileSpec,
+            resolved_file_spec: lldb.SBFileSpec,
+        ):
+            resolved_file_spec.SetDirectory(str(self.input_dir))
+            resolved_file_spec.SetFilename(SOURCE_NEW_NON_EXISTENT_FILE)
+
+            return lldb.SBError()
+
+        # SetResolveSourceFileCallback should succeed and frame.GetLineEntry will return the original file spec
+        self.assertTrue(
+            self.platform.SetResolveSourceFileCallback(
+                test_source_file_callback
+            ).Success()
+        )
+
+        # Get resolved source file spec from frame0
+        resolved_source_file_spec = self.get_source_file_for_frame()
+
+        # Check if the source file spec is resolved to the original file spec
+        self.assertEqual(resolved_source_file_spec, self.original_source_file_spec)
+        self.assertFalse(resolved_source_file_spec.Exists())
+
+    def test_return_new_existent_file(self):
+        # The callback should return a valid SBFileSpec and file exists.
+        # frame.GetLineEntry should return the new file spec.
+
+        # Resolve Source File Callback
+        def test_source_file_callback(
+            module_sp: lldb.SBModule,
+            original_file_spec: lldb.SBFileSpec,
+            resolved_file_spec: lldb.SBFileSpec,
+        ):
+            resolved_file_spec.SetDirectory(str(self.input_dir))
+            resolved_file_spec.SetFilename(SOURCE_NEW_FILE)
+
+            return lldb.SBError()
+
+        # SetResolveSourceFileCallback should succeed and frame.GetLineEntry will return the new file spec from callback
+        self.assertTrue(
+            self.platform.SetResolveSourceFileCallback(
+                test_source_file_callback
+            ).Success()
+        )
+
+        # Get resolved source file spec from frame0
+        resolved_source_file_spec = self.get_source_file_for_frame()
+
+        # Check if the source file spec is resolved to the file set in callback
+        self.assertEqual(resolved_source_file_spec, self.new_source_file_spec)
+        self.assertFalse(self.original_source_file_spec.Exists())
+        self.assertTrue(resolved_source_file_spec.Exists())
diff --git a/lldb/test/API/python_api/sbplatform/test.exe b/lldb/test/API/python_api/sbplatform/test.exe
new file mode 100755
index 00000000000000..ee23081015ce79
Binary files /dev/null and b/lldb/test/API/python_api/sbplatform/test.exe differ
diff --git a/lldb/test/API/python_api/sbplatform/test_new.cpp b/lldb/test/API/python_api/sbplatform/test_new.cpp
new file mode 100644
index 00000000000000..e9ffc97618f5d1
--- /dev/null
+++ b/lldb/test/API/python_api/sbplatform/test_new.cpp
@@ -0,0 +1,15 @@
+#include <iostream>
+
+int sum(int a, int b) {
+  return a + b; // Find the line number of function sum here.
+}
+
+int main() {
+
+  int a = 2;
+  int b = 3;
+  int c = sum(a, b);
+
+  std::cout << "c is " << c << std::endl;
+  return 0;
+}

@rchamala
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants