internal: add lldb inline comment tool

This commit is contained in:
Jarred Sumner
2025-08-05 03:23:16 -07:00
parent dfe1a1848a
commit 198d7c3b19
3 changed files with 467 additions and 1 deletions

4
.gitignore vendored
View File

@@ -184,4 +184,6 @@ codegen-for-zig-team.tar.gz
*.sock
scratch*.{js,ts,tsx,cjs,mjs}
*.bun-build
*.bun-build
scripts/lldb-inline

View File

@@ -0,0 +1,409 @@
/*
* LLDB Inline Debug Tool
*
* This tool allows you to add inline debug points in your code using comments:
* // LOG: message here
* // LOG: variable value is {variable_name}
*
* The tool will set non-stopping breakpoints at these locations and print
* the messages when hit, without interrupting program execution.
*
* BUILD INSTRUCTIONS:
* ------------------
* On macOS with Homebrew LLVM:
* c++ -std=c++17 -o lldb-inline lldb-inline-tool.cpp \
* -llldb \
* -L/opt/homebrew/opt/llvm/lib \
* -I/opt/homebrew/opt/llvm/include \
* -Wl,-rpath,/opt/homebrew/opt/llvm/lib
*
* On Linux:
* c++ -std=c++17 -o lldb-inline lldb-inline-tool.cpp \
* -llldb \
* -L/usr/lib/llvm-14/lib \
* -I/usr/lib/llvm-14/include
*
* USAGE:
* ------
* ./lldb-inline <executable> [args...]
*
* The tool searches for // LOG: comments in all source files listed in
* cmake/sources/ *.txt and sets breakpoints at those locations.
*/
#include <lldb/API/LLDB.h>
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <regex>
#include <unistd.h>
#include <glob.h>
#include <fstream>
#include <chrono>
#include <set>
#include <thread>
#include <atomic>
#include <signal.h>
#include <errno.h>
extern char **environ;
using namespace lldb;
struct DebugPoint {
std::string file;
int line;
int column;
enum Type { LOG, VAR } type;
std::string data;
};
std::vector<DebugPoint> debugPoints;
bool logpointCallback(void *baton, SBProcess &process, SBThread &thread, SBBreakpointLocation &location) {
auto start = std::chrono::high_resolution_clock::now();
auto* point = static_cast<DebugPoint*>(baton);
std::cout << point->file << ":" << point->line << ":" << point->column << " ";
// Parse the log message for {expressions}
std::string msg = point->data;
size_t pos = 0;
while ((pos = msg.find('{', pos)) != std::string::npos) {
size_t endPos = msg.find('}', pos);
if (endPos == std::string::npos) {
break;
}
// Extract expression
std::string expr = msg.substr(pos + 1, endPos - pos - 1);
// Evaluate expression
SBFrame frame = thread.GetFrameAtIndex(0);
SBValue result = frame.EvaluateExpression(expr.c_str());
std::string value;
if (result.GetError().Success() && result.GetValue()) {
value = result.GetValue();
} else {
value = "<error>";
}
// Replace {expression} with value
msg.replace(pos, endPos - pos + 1, value);
pos += value.length();
}
std::cout << msg << std::endl;
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Breakpoint callback took: " << duration.count() << "ms" << std::endl;
// Don't stop
return false;
}
std::vector<std::string> getSourceFiles() {
std::vector<std::string> files;
// Read cmake source files
glob_t globbuf;
if (glob("cmake/sources/*.txt", 0, nullptr, &globbuf) == 0) {
for (size_t i = 0; i < globbuf.gl_pathc; i++) {
std::ifstream file(globbuf.gl_pathv[i]);
std::string line;
while (std::getline(file, line)) {
if (!line.empty() && line[0] != '#' && line.find("${") == std::string::npos) {
files.push_back(line);
}
}
}
globfree(&globbuf);
}
return files;
}
void findDebugPoints() {
// Get source files
auto files = getSourceFiles();
if (files.empty()) {
return;
}
// Create temp file with list of files
std::string tmpfile = "/tmp/lldb-inline-files.txt";
std::ofstream out(tmpfile);
for (const auto& file : files) {
out << file << std::endl;
}
out.close();
// Use ripgrep with limited threads for speed
std::string cmd = "cat " + tmpfile + " | xargs rg -j4 --line-number --column --no-heading --color=never '//\\s*LOG:'";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
unlink(tmpfile.c_str());
return;
}
char buffer[1024];
std::regex logRegex(".*//\\s*LOG:\\s*(.+)");
while (fgets(buffer, sizeof(buffer), pipe)) {
std::string line(buffer);
// Remove trailing newline
if (!line.empty() && line.back() == '\n') {
line.pop_back();
}
// Parse ripgrep output: file:line:column:text
size_t pos1 = line.find(':');
if (pos1 == std::string::npos) continue;
size_t pos2 = line.find(':', pos1 + 1);
if (pos2 == std::string::npos) continue;
size_t pos3 = line.find(':', pos2 + 1);
if (pos3 == std::string::npos) continue;
DebugPoint point;
point.file = line.substr(0, pos1);
point.line = std::stoi(line.substr(pos1 + 1, pos2 - pos1 - 1));
point.column = std::stoi(line.substr(pos2 + 1, pos3 - pos2 - 1));
std::string text = line.substr(pos3 + 1);
std::smatch match;
if (std::regex_match(text, match, logRegex)) {
point.type = DebugPoint::LOG;
point.data = match[1]; // The message is in capture group 1
// Trim whitespace
point.data.erase(0, point.data.find_first_not_of(" \t"));
point.data.erase(point.data.find_last_not_of(" \t") + 1);
debugPoints.push_back(point);
}
}
pclose(pipe);
unlink(tmpfile.c_str());
}
int main(int argc, char* argv[]) {
if (argc < 2) {
return 1;
}
const char* executable = argv[1];
// Find debug points
auto start = std::chrono::high_resolution_clock::now();
findDebugPoints();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Ripgrep search took: " << duration.count() << "ms" << std::endl;
if (debugPoints.empty()) {
return 1;
}
// Initialize LLDB
start = std::chrono::high_resolution_clock::now();
SBDebugger::Initialize();
SBDebugger debugger = SBDebugger::Create(false); // Don't read .lldbinit
debugger.SetAsync(true);
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "LLDB init took: " << duration.count() << "ms" << std::endl;
// Keep LLDB's stdio handling enabled
SBCommandInterpreter interpreter = debugger.GetCommandInterpreter();
SBCommandReturnObject result;
interpreter.HandleCommand("settings set target.disable-stdio false", result);
interpreter.HandleCommand("settings set symbols.load-on-demand true", result);
interpreter.HandleCommand("settings set target.preload-symbols false", result);
interpreter.HandleCommand("settings set symbols.enable-external-lookup false", result);
interpreter.HandleCommand("settings set target.auto-import-clang-modules false", result);
interpreter.HandleCommand("settings set target.detach-on-error true", result);
// Create target
SBError error;
char cwd[PATH_MAX];
getcwd(cwd, sizeof(cwd));
std::string fullPath = executable;
if (fullPath[0] != '/') {
fullPath = std::string(cwd) + "/" + executable;
}
start = std::chrono::high_resolution_clock::now();
SBTarget target = debugger.CreateTarget(fullPath.c_str(), nullptr, nullptr, false, error);
if (!target.IsValid()) {
std::cerr << "Failed to create target: " << error.GetCString() << std::endl;
return 1;
}
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Create target took: " << duration.count() << "ms" << std::endl;
// Set breakpoints
start = std::chrono::high_resolution_clock::now();
for (auto& point : debugPoints) {
std::string absPath = point.file;
if (absPath[0] != '/') {
absPath = std::string(cwd) + "/" + point.file;
}
SBBreakpoint bp = target.BreakpointCreateByLocation(absPath.c_str(), point.line);
if (bp.IsValid()) {
bp.SetCallback(logpointCallback, &point);
}
}
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Set breakpoints took: " << duration.count() << "ms" << std::endl;
// Build args
std::vector<const char*> args;
for (int i = 2; i < argc; i++) {
args.push_back(argv[i]);
}
args.push_back(nullptr);
// Launch process with proper settings
SBLaunchInfo launch_info(args.data());
launch_info.SetWorkingDirectory(cwd);
launch_info.SetLaunchFlags(0); // Don't disable stdio
// Pass through environment variables from parent
SBEnvironment env = launch_info.GetEnvironment();
for (char **p = environ; *p != nullptr; p++) {
env.PutEntry(*p);
}
launch_info.SetEnvironment(env, false);
start = std::chrono::high_resolution_clock::now();
SBProcess process = target.Launch(launch_info, error);
if (!process.IsValid()) {
std::cerr << "Failed to launch process: " << error.GetCString() << std::endl;
return 1;
}
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Launch process took: " << duration.count() << "ms" << std::endl;
// lldb::pid_t launchedPid = process.GetProcessID();
// std::cerr << "Launched process with PID: " << launchedPid << std::endl;
// Handle events properly
SBListener listener = debugger.GetListener();
start = std::chrono::high_resolution_clock::now();
auto lastOutput = start;
bool done = false;
bool gotOutput = false;
int eventCount = 0;
while (!done) {
SBEvent event;
if (listener.WaitForEvent(0, event)) { // Non-blocking
eventCount++;
auto eventTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(eventTime - start);
StateType state = SBProcess::GetStateFromEvent(event);
// std::cerr << "Event #" << eventCount << " at " << elapsed.count() << "ms: state=" << state << std::endl;
if (state == eStateExited) {
auto exitTime = std::chrono::high_resolution_clock::now();
auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(exitTime - start);
// std::cerr << "Process exited with code: " << process.GetExitStatus() << " after " << totalTime.count() << "ms in event loop" << std::endl;
}
switch (state) {
case eStateStopped:
process.Continue();
break;
case eStateRunning:
break;
case eStateExited:
case eStateCrashed:
case eStateDetached:
// std::cerr << "Exiting immediately on state: " << state << std::endl;
// Just exit immediately - skip all cleanup
exit(process.GetExitStatus());
break;
default:
break;
}
} else {
// No event, check if process is done
StateType currentState = process.GetState();
if (currentState == eStateExited || currentState == eStateCrashed || currentState == eStateDetached) {
done = true;
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - lastOutput);
// std::cerr << "Process already exited, detected by polling. Time from last output: " << duration.count() << "ms" << std::endl;
}
}
// Read and forward stdout/stderr
char buffer[1024];
size_t num_bytes;
bool hadStdout = false;
while ((num_bytes = process.GetSTDOUT(buffer, sizeof(buffer)-1)) > 0) {
buffer[num_bytes] = '\0';
std::cout << buffer;
std::cout.flush();
lastOutput = std::chrono::high_resolution_clock::now();
gotOutput = true;
hadStdout = true;
}
bool hadStderr = false;
while ((num_bytes = process.GetSTDERR(buffer, sizeof(buffer)-1)) > 0) {
buffer[num_bytes] = '\0';
std::cerr << buffer;
std::cerr.flush();
lastOutput = std::chrono::high_resolution_clock::now();
gotOutput = true;
hadStderr = true;
}
// Poll process state every iteration
StateType currentState = process.GetState();
if (currentState == eStateExited || currentState == eStateCrashed || currentState == eStateDetached) {
// Process has exited, break out of loop
done = true;
} else {
// Small sleep to avoid busy-waiting
usleep(10000); // 10ms
}
}
int exit_code = process.GetExitStatus();
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Total event loop time: " << duration.count() << "ms" << std::endl;
// Cleanup
start = std::chrono::high_resolution_clock::now();
SBDebugger::Destroy(debugger);
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// std::cerr << "Debugger destroy took: " << duration.count() << "ms" << std::endl;
SBDebugger::Terminate();
return exit_code;
}

55
scripts/lldb-inline.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# LLDB Inline Debug Tool Build & Run Script
#
# This script builds the lldb-inline tool if needed and runs it.
# Usage: ./scripts/lldb-inline.sh <executable> [args...]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOL_SOURCE="$SCRIPT_DIR/lldb-inline-tool.cpp"
TOOL_BINARY="$SCRIPT_DIR/lldb-inline"
# Check if we need to rebuild
if [ ! -f "$TOOL_BINARY" ] || [ "$TOOL_SOURCE" -nt "$TOOL_BINARY" ]; then
# Detect OS and build accordingly
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS with Homebrew LLVM
c++ -std=c++17 -o "$TOOL_BINARY" "$TOOL_SOURCE" \
-llldb \
-L/opt/homebrew/opt/llvm/lib \
-I/opt/homebrew/opt/llvm/include \
-Wl,-rpath,/opt/homebrew/opt/llvm/lib >/dev/null 2>&1
else
# Linux - try to find LLVM installation
LLVM_DIR=""
for version in 18 17 16 15 14 13 12; do
if [ -d "/usr/lib/llvm-$version" ]; then
LLVM_DIR="/usr/lib/llvm-$version"
break
fi
done
if [ -z "$LLVM_DIR" ] && [ -d "/usr/lib/llvm" ]; then
LLVM_DIR="/usr/lib/llvm"
fi
if [ -z "$LLVM_DIR" ]; then
# Try pkg-config as fallback
LLDB_CFLAGS=$(pkg-config --cflags lldb 2>/dev/null)
LLDB_LIBS=$(pkg-config --libs lldb 2>/dev/null)
c++ -std=c++17 -o "$TOOL_BINARY" "$TOOL_SOURCE" \
$LLDB_CFLAGS $LLDB_LIBS >/dev/null 2>&1
else
c++ -std=c++17 -o "$TOOL_BINARY" "$TOOL_SOURCE" \
-llldb \
-L"$LLVM_DIR/lib" \
-I"$LLVM_DIR/include" >/dev/null 2>&1
fi
fi
if [ $? -ne 0 ]; then
exit 1
fi
fi
# Run the tool with all arguments
exec "$TOOL_BINARY" "$@"