Mixed Debugging with Dart and Rust
- Brent Lewis
- May 29, 2023
- 3 min read
Updated: Jun 15, 2023
If you've ever worked with native interop, be it with Dart and Rust or otherwise, you recognize the value in mixed debugging. The idea is that you can step into a Dart function call and the debugger jumps into some Rust code. It's a compelling workflow. While getting the Visual Studio Code debugger running in just Dart or just Rust is very straightforward, that's not the case for starting a mixed debugging session. In this devlog, I document the automation of mixed debugging in Visual Studio Code. I haven't tested this on Windows or OSX yet, but it mainly works.
Start with a typical Dart debug configuration in the launch.json. Pressing F5 should be enough to start Dart debugging and hit a breakpoint or two. The Visual Studio Code extension CodeLLDB is a popular native/Rust debugger. Once installed, it can attach to the process:
Launch Dart debugging as usual
Determine the inferior process ID
Use the "LLDB: Attach to Process..." command in VS Code command pallet to attach to the process ID.
Symbols are loaded, step stepsf, and it generally works. Determining a PID and running a command is manual though, and these steps will be automated when F5 is pressed. The documentation for CodeLLDB says that it can be launched as a URI.
vscode://vadimcn.vscode-lldb/launch/config?{'name':name,'request':'attach','pid':123}
It doesn't have to be launched from within VS Code; the Run dialog or console suffice. When this URI launches, VS Code will ask for confirmation. Accept the dialogue to start debugging.
VS Code can automate running a URI when F5 (run) is pressed. A task can perform this step. Add a "preLaunchTask" to the Dart debug configuration:
{ "name": "Sandbox", "type": "dart", "request": "launch", "program": "${workspaceFolder}/frb_example/sandbox/dart/lib/main.dart", "args": [ "${workspaceFolder}/target/debug/libflutter_rust_bridge_sandbox.so" ], "preLaunchTask": "Dart attach native" },
The new "Dart attach native" task will run before Dart debugging starts. There are some details that complicate authoring the task.
A task can only run a single command. Since there are several steps to complete, and being that cross-platform is a good goal, shell scripting isn't a good solution.
VS Code will wait for the preLaunchTask to finish before it will launch Dart. Since the URI launch needs to happen after the Dart debugger starts, a separate process will need to stick around and work in the background. What's worse, VS Code will kill any remaining child processes of the task when it ends, so the background worker also needs to be detached.
The task is in Python so it can be used across platforms, which addresses the first bullet. The preLaunchTask hook in the launch.json is kept simple, leaving the heavy lifting to the Python script. The below file goes in my_project/.vscode/tasks.json:
{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "Dart attach native", "type": "shell", "command": "python3 ${workspaceFolder}/.vscode/attach_native_dart.py", "presentation": { "reveal": "silent" } } ] }
The second bullet is addressed with nohup on Posix-like systems and subprocess.CREATE_NEW_PROCESS_GROUP on Windows. The Python script re-launches itself as a detached background process, letting the main process exit, thereby ending the task and letting the Dart debugger start. The background process will search for some instance of the Dart VM that was passed a .so, .dll, or .dylib file in its command line (a native shared library). If it finds such a process, it puts the process ID in the URI from the beginning and launches it. If it doesn't find it after 30 seconds, it pops up a tkinter error dialog. my_project/.vscode/attach_native_dart.py:
import os import platform import psutil import re import subprocess import sys import time try: import tkinter as tk from tkinter import messagebox except ImportError: print("tkinter not found, error dialogs will not be shown") def launch_uri(filename): if platform.system() == 'Linux': subprocess.run(['xdg-open', filename]) elif platform.system() == 'Darwin': subprocess.run(['open', filename]) elif platform.system() == 'Windows': os.startfile(filename) else: print(f'Platform {platform.system()} not supported') def find_dart_process(): start = time.time() while time.time() - start < 30: for proc in psutil.process_iter(['pid', 'cmdline']): try: cmdline = ' '.join(proc.info['cmdline']) if 'dart' in cmdline and ( re.search(r'\.so\b', cmdline) # Linux or re.search(r'\.dll\b', cmdline) # Windows or re.search(r'\.dylib\b', cmdline) # macOS ): return proc.info['pid'] except (psutil.NoSuchProcess, psutil.AccessDenied): pass raise Exception("Failed to find Dart process for native debugging") def present_error(message): root = tk.Tk() root.withdraw() # hides the main window messagebox.showerror("Error", message) root.destroy() def attach_codelldb(pid: int): uri = f"vscode://vadimcn.vscode-lldb/launch/config?{{'name':'Dart native','request':'attach','pid':{pid}}}" print("Opening " + uri) launch_uri(uri) if len(sys.argv) == 1: if platform.system() == "Windows": subprocess.Popen([__file__, "fork"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) subprocess.Popen(["nohup", __file__, "fork"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) sys.exit(0) try: pid = find_dart_process() attach_codelldb(pid) except Exception as e: present_error(str(e)) sys.exit(1) That's it! Make sure to install psutils and tkinter if not already installed, and mark the Python script as executable. Also, this relies on there being a single identifiable Dart process with a native shared library passed on the command line. If there's more than one, you might end up attaching CodeLLDB to the wrong process. You can tweak the search logic of the find_dart_process function in the Python script if you need something more nuanced. Have fun mixed debugging!
Comments