diff --git a/lldb/CMakeLists.txt b/lldb/CMakeLists.txt index e3b72e94d4beb..a852e581160b5 100644 --- a/lldb/CMakeLists.txt +++ b/lldb/CMakeLists.txt @@ -95,7 +95,7 @@ if (LLDB_ENABLE_LUA) CACHE STRING "Path where Lua modules are installed, relative to install prefix") endif () -if (LLDB_ENABLE_PYTHON OR LLDB_ENABLE_LUA) +if (LLDB_ENABLE_PYTHON OR LLDB_ENABLE_LUA OR LLDB_ENABLE_JAVASCRIPT) add_subdirectory(bindings) endif () @@ -150,6 +150,16 @@ if (LLDB_ENABLE_LUA) finish_swig_lua("lldb-lua" "${lldb_lua_bindings_dir}" "${LLDB_LUA_CPATH}") endif() +if (LLDB_ENABLE_JAVASCRIPT) + if(LLDB_BUILD_FRAMEWORK) + set(lldb_javascript_target_dir "${LLDB_FRAMEWORK_ABSOLUTE_BUILD_DIR}/LLDB.framework/Resources/JavaScript") + else() + set(lldb_javascript_target_dir "${CMAKE_BINARY_DIR}/${CMAKE_CFG_INTDIR}/lib/javascript") + endif() + get_target_property(lldb_javascript_bindings_dir swig_wrapper_javascript BINARY_DIR) + finish_swig_javascript("lldb-javascript" "${lldb_javascript_bindings_dir}" "${lldb_javascript_target_dir}") +endif() + set(LLDB_INCLUDE_UNITTESTS ON) if (NOT TARGET llvm_gtest) set(LLDB_INCLUDE_UNITTESTS OFF) diff --git a/lldb/bindings/CMakeLists.txt b/lldb/bindings/CMakeLists.txt index bec694e43bd7b..984614a1238aa 100644 --- a/lldb/bindings/CMakeLists.txt +++ b/lldb/bindings/CMakeLists.txt @@ -57,3 +57,7 @@ endif() if (LLDB_ENABLE_LUA) add_subdirectory(lua) endif() + +if (LLDB_ENABLE_JAVASCRIPT) + add_subdirectory(javascript) +endif() diff --git a/lldb/bindings/javascript/CMakeLists.txt b/lldb/bindings/javascript/CMakeLists.txt new file mode 100644 index 0000000000000..7356625f71a90 --- /dev/null +++ b/lldb/bindings/javascript/CMakeLists.txt @@ -0,0 +1,69 @@ +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/LLDBWrapJavaScript.cpp + DEPENDS ${SWIG_SOURCES} + DEPENDS ${SWIG_INTERFACES} + DEPENDS ${SWIG_HEADERS} + DEPENDS lldb-sbapi-dwarf-enums + COMMAND ${SWIG_EXECUTABLE} + ${SWIG_COMMON_FLAGS} + -I${CMAKE_CURRENT_SOURCE_DIR} + -javascript + -v8 + -w503 + -outdir ${CMAKE_CURRENT_BINARY_DIR} + -o ${CMAKE_CURRENT_BINARY_DIR}/LLDBWrapJavaScript.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/javascript.swig + VERBATIM + COMMENT "Building LLDB JavaScript wrapper") + +add_custom_target(swig_wrapper_javascript ALL DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/LLDBWrapJavaScript.cpp +) + +function(create_javascript_package swig_target working_dir pkg_dir) + cmake_parse_arguments(ARG "NOINIT" "" "FILES" ${ARGN}) + add_custom_command(TARGET ${swig_target} POST_BUILD VERBATIM + COMMAND ${CMAKE_COMMAND} -E make_directory ${pkg_dir} + WORKING_DIRECTORY ${working_dir}) +endfunction() + +function(finish_swig_javascript swig_target lldb_javascript_bindings_dir lldb_javascript_target_dir) + add_custom_target(${swig_target} ALL VERBATIM + COMMAND ${CMAKE_COMMAND} -E make_directory ${lldb_javascript_target_dir} + DEPENDS swig_wrapper_javascript liblldb + COMMENT "LLDB JavaScript API") + if(LLDB_BUILD_FRAMEWORK) + set(LIBLLDB_SYMLINK_DEST "${LLDB_FRAMEWORK_ABSOLUTE_BUILD_DIR}/LLDB.framework/LLDB") + else() + set(LIBLLDB_SYMLINK_DEST "${LLVM_SHLIB_OUTPUT_INTDIR}/liblldb${CMAKE_SHARED_LIBRARY_SUFFIX}") + endif() + if(WIN32) + set(LIBLLDB_SYMLINK_OUTPUT_FILE "lldb.dll") + else() + set(LIBLLDB_SYMLINK_OUTPUT_FILE "lldb.so") + endif() + create_relative_symlink(${swig_target} ${LIBLLDB_SYMLINK_DEST} + ${lldb_javascript_target_dir} ${LIBLLDB_SYMLINK_OUTPUT_FILE}) + set(lldb_javascript_library_target "${swig_target}-library") + add_custom_target(${lldb_javascript_library_target}) + add_dependencies(${lldb_javascript_library_target} ${swig_target}) + + # Ensure we do the JavaScript post-build step when building lldb. + add_dependencies(lldb ${swig_target}) + + if(LLDB_BUILD_FRAMEWORK) + set(LLDB_JAVASCRIPT_INSTALL_PATH ${LLDB_FRAMEWORK_INSTALL_DIR}/LLDB.framework/Resources/JavaScript) + else() + set(LLDB_JAVASCRIPT_INSTALL_PATH lib/javascript) + endif() + install(DIRECTORY ${lldb_javascript_target_dir}/ + DESTINATION ${LLDB_JAVASCRIPT_INSTALL_PATH} + COMPONENT ${lldb_javascript_library_target}) + + set(lldb_javascript_library_install_target "install-${lldb_javascript_library_target}") + if (NOT LLVM_ENABLE_IDE) + add_llvm_install_targets(${lldb_javascript_library_install_target} + COMPONENT ${lldb_javascript_library_target} + DEPENDS ${lldb_javascript_library_target}) + endif() +endfunction() diff --git a/lldb/bindings/javascript/javascript-swigsafecast.swig b/lldb/bindings/javascript/javascript-swigsafecast.swig new file mode 100644 index 0000000000000..5d2b50f1fd249 --- /dev/null +++ b/lldb/bindings/javascript/javascript-swigsafecast.swig @@ -0,0 +1,8 @@ +/* + Safe casting for JavaScript SWIG bindings +*/ + +// This file provides safe type casting between LLDB types +// Similar to lua-swigsafecast.swig and python-swigsafecast.swig + +// TODO: Implement safe casting functions as needed diff --git a/lldb/bindings/javascript/javascript-typemaps.swig b/lldb/bindings/javascript/javascript-typemaps.swig new file mode 100644 index 0000000000000..cc11067e712bd --- /dev/null +++ b/lldb/bindings/javascript/javascript-typemaps.swig @@ -0,0 +1,48 @@ +/* + JavaScript-specific typemaps for LLDB +*/ + +%header %{ +#include +%} + +// Typemap for char ** (string arrays) - used in LaunchSimple, Launch, etc. +// Converts JavaScript arrays to C string arrays +%typemap(in) char ** { + if ($input->IsArray()) { + v8::Local array = v8::Local::Cast($input); + uint32_t length = array->Length(); + $1 = (char **)malloc((length + 1) * sizeof(char *)); + + for (uint32_t i = 0; i < length; i++) { + v8::Local element; + if (array->Get(SWIGV8_CURRENT_CONTEXT(), i).ToLocal(&element)) { + if (element->IsString()) { + v8::String::Utf8Value str(SWIGV8_CURRENT_CONTEXT()->GetIsolate(), element); + $1[i] = strdup(*str); + } else { + free($1); + SWIG_exception_fail(SWIG_TypeError, "Array elements must be strings"); + } + } + } + $1[length] = NULL; + } else if ($input->IsNull() || $input->IsUndefined()) { + $1 = NULL; + } else { + SWIG_exception_fail(SWIG_TypeError, "Expected array of strings or null"); + } +} + +%typemap(freearg) char ** { + if ($1) { + for (int i = 0; $1[i] != NULL; i++) { + free($1[i]); + } + free($1); + } +} + +%typemap(typecheck, precedence=SWIG_TYPECHECK_STRING_ARRAY) char ** { + $1 = $input->IsArray() || $input->IsNull() || $input->IsUndefined(); +} diff --git a/lldb/bindings/javascript/javascript-wrapper.swig b/lldb/bindings/javascript/javascript-wrapper.swig new file mode 100644 index 0000000000000..af952201ddd05 --- /dev/null +++ b/lldb/bindings/javascript/javascript-wrapper.swig @@ -0,0 +1,12 @@ +/* + JavaScript-specific wrapper functions for LLDB +*/ + +// This file will contain JavaScript-specific wrapper code +// to bridge between LLDB's C++ API and JavaScript/V8 + +// TODO: Add wrapper functions for: +// - Breakpoint callbacks +// - Watchpoint callbacks +// - Custom commands +// - Data formatters diff --git a/lldb/bindings/javascript/javascript.swig b/lldb/bindings/javascript/javascript.swig new file mode 100644 index 0000000000000..098c04ad8d365 --- /dev/null +++ b/lldb/bindings/javascript/javascript.swig @@ -0,0 +1,30 @@ +/* + lldb.swig + + This is the input file for SWIG, to create the appropriate C++ wrappers and + functions for JavaScript (V8/Node.js), to enable them to call the + liblldb Script Bridge functions. +*/ + +%module lldb + +%include +%include "javascript-typemaps.swig" +%include "macros.swig" +%include "headers.swig" + +%{ +#include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" +#include "../bindings/javascript/javascript-swigsafecast.swig" +#include "../source/Plugins/ScriptInterpreter/JavaScript/SWIGJavaScriptBridge.h" + +// required headers for typemaps +#include "lldb/Host/File.h" + +using namespace lldb_private; +using namespace lldb; +%} + +%include "interfaces.swig" +%include "javascript-wrapper.swig" diff --git a/lldb/cmake/modules/FindV8.cmake b/lldb/cmake/modules/FindV8.cmake new file mode 100644 index 0000000000000..d6ce23feddef1 --- /dev/null +++ b/lldb/cmake/modules/FindV8.cmake @@ -0,0 +1,71 @@ +#.rst: +# FindV8 +# ------ +# +# Find V8 JavaScript engine +# +# This module will search for V8 in standard system locations, or use +# user-specified paths. Users can override the search by setting: +# -DV8_INCLUDE_DIR=/path/to/v8/include +# -DV8_LIBRARIES=/path/to/libv8.so (or libv8_monolith.a) +# +# The module defines: +# V8_FOUND - System has V8 +# V8_INCLUDE_DIR - V8 include directory +# V8_LIBRARIES - V8 libraries to link against + +if(V8_LIBRARIES AND V8_INCLUDE_DIR) + set(V8_FOUND TRUE) + if(NOT V8_FIND_QUIETLY) + message(STATUS "Found V8: ${V8_INCLUDE_DIR}") + message(STATUS "Found V8 library: ${V8_LIBRARIES}") + set(V8_FIND_QUIETLY TRUE CACHE BOOL "Suppress repeated V8 find messages" FORCE) + endif() +else() + # Try to find system V8 + find_path(V8_INCLUDE_DIR + NAMES v8.h + PATHS + # Standard system locations + /usr/include + /usr/local/include + /opt/v8/include + # Homebrew on macOS + /opt/homebrew/include + /usr/local/opt/v8/include + PATH_SUFFIXES + v8 + DOC "V8 include directory" + ) + + find_library(V8_LIBRARIES + NAMES v8_monolith v8 v8_libbase v8_libplatform + PATHS + # Standard system locations + /usr/lib + /usr/local/lib + /opt/v8/lib + # Homebrew on macOS + /opt/homebrew/lib + /usr/local/opt/v8/lib + DOC "V8 library" + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(V8 + FOUND_VAR + V8_FOUND + REQUIRED_VARS + V8_INCLUDE_DIR + V8_LIBRARIES) + + if(V8_FOUND) + mark_as_advanced(V8_LIBRARIES V8_INCLUDE_DIR) + message(STATUS "Found V8: ${V8_INCLUDE_DIR}") + if(V8_LIBRARIES) + message(STATUS "Found V8 library: ${V8_LIBRARIES}") + else() + message(STATUS "V8 headers found (library may need to be built or specified manually)") + endif() + endif() +endif() diff --git a/lldb/cmake/modules/LLDBConfig.cmake b/lldb/cmake/modules/LLDBConfig.cmake index 4b568d27c4709..e42522e8b8765 100644 --- a/lldb/cmake/modules/LLDBConfig.cmake +++ b/lldb/cmake/modules/LLDBConfig.cmake @@ -62,6 +62,7 @@ add_optional_dependency(LLDB_ENABLE_CURSES "Enable curses support in LLDB" Curse add_optional_dependency(LLDB_ENABLE_LZMA "Enable LZMA compression support in LLDB" LibLZMA LIBLZMA_FOUND) add_optional_dependency(LLDB_ENABLE_LUA "Enable Lua scripting support in LLDB" LuaAndSwig LUAANDSWIG_FOUND) add_optional_dependency(LLDB_ENABLE_PYTHON "Enable Python scripting support in LLDB" PythonAndSwig PYTHONANDSWIG_FOUND) +add_optional_dependency(LLDB_ENABLE_JAVASCRIPT "Enable JavaScript scripting support in LLDB" V8 V8_FOUND) add_optional_dependency(LLDB_ENABLE_LIBXML2 "Enable Libxml 2 support in LLDB" LibXml2 LIBXML2_FOUND VERSION ${LLDB_LIBXML2_VERSION}) add_optional_dependency(LLDB_ENABLE_FBSDVMCORE "Enable libfbsdvmcore support in LLDB" FBSDVMCore FBSDVMCore_FOUND QUIET) diff --git a/lldb/docs/index.rst b/lldb/docs/index.rst index a981c0ab8d6e9..bfe39dc9d5297 100644 --- a/lldb/docs/index.rst +++ b/lldb/docs/index.rst @@ -27,7 +27,9 @@ with GDB there is a cheat sheet listing common tasks and their LLDB equivalent in the `GDB to LLDB command map `_. There are also multiple resources on how to script LLDB using Python: the -:doc:`use/python-reference` is a great starting point for that. +:doc:`use/python-reference` is a great starting point for that. LLDB also +supports scripting with JavaScript through the V8 engine (see +`JavaScript Reference `_). Compiler Integration Benefits ----------------------------- @@ -148,6 +150,7 @@ interesting areas to contribute to lldb. use/python use/python-reference + use/javascript-reference Python API Python Extensions diff --git a/lldb/docs/resources/build.rst b/lldb/docs/resources/build.rst index 0db8c92ad49d6..5d6ee39ea6164 100644 --- a/lldb/docs/resources/build.rst +++ b/lldb/docs/resources/build.rst @@ -66,6 +66,8 @@ CMake configuration error. +-------------------+--------------------------------------------------------------+--------------------------+ | Lua | Lua scripting. Lua 5.3 and 5.4 are supported. | ``LLDB_ENABLE_LUA`` | +-------------------+--------------------------------------------------------------+--------------------------+ +| JavaScript | JavaScript scripting via V8 engine. Experimental. | ``LLDB_ENABLE_JAVASCRIPT``| ++-------------------+--------------------------------------------------------------+--------------------------+ Depending on your platform and package manager, one might run any of the commands below. diff --git a/lldb/docs/use/javascript-reference.md b/lldb/docs/use/javascript-reference.md new file mode 100644 index 0000000000000..a73befa02f224 --- /dev/null +++ b/lldb/docs/use/javascript-reference.md @@ -0,0 +1,263 @@ +# JavaScript Reference + +LLDB has extensive support for interacting with JavaScript through the V8 +JavaScript engine. This document describes how to use JavaScript scripting +within LLDB and provides reference documentation for the JavaScript API. + +## Using JavaScript in LLDB + +LLDB's JavaScript support is built on top of the V8 JavaScript engine, the same +engine that powers Node.js and Chrome. This provides full ES2020+ language +support with modern JavaScript features. + +### Interactive JavaScript + +JavaScript can be run interactively in LLDB. First, set JavaScript as the script language, then use the `script` command: + +``` +(lldb) settings set script-lang javascript +(lldb) script +>>> let message = "Hello from JavaScript!"; +>>> console.log(message); +Hello from JavaScript! +>>> lldb.debugger.GetVersionString() +lldb version 18.0.0 +``` + +### Running JavaScript from Files + +You can execute JavaScript files using the `command script import` command: + +``` +(lldb) command script import /path/to/myscript.js +``` + +The JavaScript file will be executed in the current LLDB context with access +to all LLDB APIs. + +### Example JavaScript Script + +Here's a simple example that demonstrates using the LLDB JavaScript API: + +```javascript +// Get the current debugger instance +let debugger = lldb.debugger; + +// Get the current target +let target = debugger.GetSelectedTarget(); + +// Get the current process +let process = target.GetProcess(); + +// Get the selected thread +let thread = process.GetSelectedThread(); + +// Get the selected frame +let frame = thread.GetSelectedFrame(); + +// Evaluate an expression +let result = frame.EvaluateExpression("myVariable"); +console.log("Value:", result.GetValue()); + +// Print all local variables +let variables = frame.GetVariables(true, true, false, false); +for (let i = 0; i < variables.GetSize(); i++) { + let variable = variables.GetValueAtIndex(i); + console.log(variable.GetName() + " = " + variable.GetValue()); +} +``` + +## The JavaScript API + +The JavaScript API provides access to all of LLDB's Script Bridge (SB) API +classes. These classes are automatically available in the `lldb` module when +running JavaScript within LLDB. + +### Global Objects + +* `lldb`: The main LLDB module containing all SB API classes +* `lldb.debugger`: The current debugger instance (shortcut to avoid passing + debugger around) +* `console`: Standard JavaScript console object for logging + +### Available Classes + +The JavaScript API includes all of LLDB's SB API classes, like `SBDebugger`, +`SBTarget`, etc. + +For complete documentation of all classes and their methods, refer to the +[C++ API documentation](https://lldb.llvm.org/cpp_reference/namespacelldb.html), +as the JavaScript API mirrors the C++ API closely. + +### Console Output + +JavaScript scripts can use the standard `console` object for output: + +```javascript +console.log("Informational message"); +console.error("Error message"); +console.warn("Warning message"); +``` + +Output from `console.log()` and other console methods will be displayed in +the LLDB command output. + +## Building LLDB with JavaScript Support + +### Prerequisites + +To build LLDB with JavaScript support, you need: + +* [V8 JavaScript Engine](https://v8.dev) (version 8.0 or later recommended) +* [SWIG](http://swig.org/) 4 or later (for generating language bindings) +* All standard LLDB build dependencies (see [build documentation](../resources/build.rst)) + +### Installing V8 + +The V8 JavaScript engine must be installed on your system. Installation methods +vary by platform: + +**Ubuntu/Debian:** + +```bash +$ sudo apt-get install libv8-dev +``` + +After installation, V8 will typically be installed in: +- Headers: `/usr/include/v8/` or `/usr/include/` +- Libraries: `/usr/lib/x86_64-linux-gnu/libv8.so` (or similar for your architecture) + +You can verify the installation with: +```bash +$ dpkg -L libv8-dev | grep -E '(include|lib)' +``` + +**macOS (using Homebrew):** + +```bash +$ brew install v8 +``` + +After installation, you can find the paths with: +```bash +$ brew info v8 +``` + +Homebrew typically installs to `/opt/homebrew/` (Apple Silicon) or `/usr/local/` (Intel). + +**Building V8 from source:** + +If V8 is not available as a package for your platform, you can build it from +source. Follow the instructions at https://v8.dev/docs/build + +### CMake Configuration + +To enable JavaScript support when building LLDB, add the following CMake +options: + +```bash +$ cmake -G Ninja \ + -DLLDB_ENABLE_JAVASCRIPT=ON \ + [other cmake options] \ + /path/to/llvm-project/llvm +``` + +The `LLDB_ENABLE_JAVASCRIPT` flag enables JavaScript scripting support. If +V8 is installed via a package manager in standard system locations, CMake +should auto-detect it. If CMake cannot find V8, you can specify the paths +manually: + +```bash +$ cmake -G Ninja \ + -DLLDB_ENABLE_JAVASCRIPT=ON \ + -DV8_INCLUDE_DIR=/path/to/v8/include \ + -DV8_LIBRARIES=/path/to/v8/lib/libv8.so \ + [other cmake options] \ + /path/to/llvm-project/llvm +``` + +where: +* `V8_INCLUDE_DIR`: Path to V8 header files +* `V8_LIBRARIES`: Path to V8 library files + +### Verifying JavaScript Support + +After building LLDB with JavaScript support, you can verify it's working: + +``` +$ lldb +(lldb) settings set script-lang javascript +(lldb) script +>>> console.log("JavaScript is working!") +JavaScript is working! +>>> lldb.debugger.GetVersionString() +lldb version 18.0.0 +``` + +If JavaScript support is not enabled, you'll see an error message when trying +to set the script language to JavaScript. + +### Build Example + +Here's a complete example of building LLDB with JavaScript support from scratch: + +```bash +# Clone the LLVM project +$ git clone https://github.com/llvm/llvm-project.git + +# Create build directory +$ mkdir llvm-build && cd llvm-build + +# Configure with JavaScript support +$ cmake -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_ENABLE_PROJECTS="clang;lldb" \ + -DLLDB_ENABLE_JAVASCRIPT=ON \ + -DV8_INCLUDE_DIR=/usr/include/v8 \ + -DV8_LIBRARIES=/usr/lib/x86_64-linux-gnu/libv8.so \ + ../llvm-project/llvm + +# Build LLDB +$ ninja lldb + +# Test JavaScript support +$ ./bin/lldb -o "settings set script-lang javascript" -o "script -e \"console.log('Hello!')\"" -o "quit" +``` + +## Differences from Python API and JavaScript Environment + +Important differences to understand: + +**Not a Node.js Environment:** + +LLDB's JavaScript environment uses the V8 engine but is **not** Node.js. This means: + +* **No module system**: `import`, `require()`, and `module.exports` are not available +* **No event loop**: Asynchronous operations like `setTimeout`, `setInterval`, `Promise.then()` callbacks are not supported +* **Limited global APIs**: Only specific functions are implemented: + * `console.log()`, `console.error()`, `console.warn()` for output + * `lldb` global object for LLDB API access + * Standard JavaScript language features (ES2020+) + +**Module Access:** + +In Python, you typically import with `import lldb`. In JavaScript, `lldb` +is automatically available as a global object without any import statement. + +Scripts should be written as self-contained synchronous code that directly uses the +`lldb` global object. + +## Known Limitations + +The JavaScript support in LLDB is not as extensive as Python. The +following features are not yet implemented: + +* Custom breakpoint callbacks in JavaScript +* Custom watchpoint callbacks in JavaScript +* Some advanced type mapping and conversions + +## Additional Resources + +* [LLDB C++ API Reference](https://lldb.llvm.org/cpp_reference/namespacelldb.html) +* [V8 JavaScript Engine Documentation](https://v8.dev/docs) +* [LLDB Python Reference](python-reference.html) (similar concepts apply to JavaScript) diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h index 4face717531b1..9e4ab5d1425e6 100644 --- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h +++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h @@ -96,6 +96,11 @@ static constexpr OptionEnumValueElement g_script_option_enumeration[] = { "lua", "Commands are in the Lua language.", }, + { + lldb::eScriptLanguageJavaScript, + "javascript", + "Commands are in the JavaScript language.", + }, { lldb::eScriptLanguageNone, "default", diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h index 1a7db8faecd94..879c25e3ae0d5 100644 --- a/lldb/include/lldb/lldb-enumerations.h +++ b/lldb/include/lldb/lldb-enumerations.h @@ -227,8 +227,17 @@ enum ScriptLanguage { eScriptLanguageNone = 0, eScriptLanguagePython, eScriptLanguageLua, + eScriptLanguageJavaScript, eScriptLanguageUnknown, +#if LLDB_ENABLE_PYTHON eScriptLanguageDefault = eScriptLanguagePython +#elif LLDB_ENABLE_LUA + eScriptLanguageDefault = eScriptLanguageLua +#elif LLDB_ENABLE_JAVASCRIPT + eScriptLanguageDefault = eScriptLanguageJavaScript +#else + eScriptLanguageDefault = eScriptLanguageNone +#endif }; /// Register numbering types. @@ -314,7 +323,7 @@ enum ConnectionStatus { eConnectionStatusNoConnection, ///< No connection eConnectionStatusLostConnection, ///< Lost connection while connected to a ///< valid connection - eConnectionStatusInterrupted ///< Interrupted read + eConnectionStatusInterrupted ///< Interrupted read }; enum ErrorType { @@ -1109,7 +1118,7 @@ enum PathType { ePathTypeGlobalLLDBTempSystemDir, ///< The LLDB temp directory for this ///< system, NOT cleaned up on a process ///< exit. - ePathTypeClangDir ///< Find path to Clang builtin headers + ePathTypeClangDir ///< Find path to Clang builtin headers }; /// Kind of member function. diff --git a/lldb/source/Commands/CommandObjectBreakpointCommand.cpp b/lldb/source/Commands/CommandObjectBreakpointCommand.cpp index a913ed5fa12b3..8e60ebce987bd 100644 --- a/lldb/source/Commands/CommandObjectBreakpointCommand.cpp +++ b/lldb/source/Commands/CommandObjectBreakpointCommand.cpp @@ -266,6 +266,7 @@ are no syntax errors may indicate that a function was declared but never called. switch (m_script_language) { case eScriptLanguagePython: case eScriptLanguageLua: + case eScriptLanguageJavaScript: m_use_script_language = true; break; case eScriptLanguageNone: diff --git a/lldb/source/Commands/CommandObjectWatchpointCommand.cpp b/lldb/source/Commands/CommandObjectWatchpointCommand.cpp index 062bf75eb8ae8..1e5543096e5a2 100644 --- a/lldb/source/Commands/CommandObjectWatchpointCommand.cpp +++ b/lldb/source/Commands/CommandObjectWatchpointCommand.cpp @@ -297,6 +297,7 @@ are no syntax errors may indicate that a function was declared but never called. switch (m_script_language) { case eScriptLanguagePython: case eScriptLanguageLua: + case eScriptLanguageJavaScript: m_use_script_language = true; break; case eScriptLanguageNone: diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index b37d9d3ed85e3..d8a6fd44c7f3f 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -147,6 +147,11 @@ static constexpr OptionEnumValueElement g_language_enumerators[] = { "python", "Select python as the default scripting language.", }, + { + eScriptLanguageJavaScript, + "javascript", + "Select javascript as the default scripting language.", + }, { eScriptLanguageDefault, "default", diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp index ca768db1199c1..b215710a545f0 100644 --- a/lldb/source/Interpreter/ScriptInterpreter.cpp +++ b/lldb/source/Interpreter/ScriptInterpreter.cpp @@ -65,6 +65,8 @@ std::string ScriptInterpreter::LanguageToString(lldb::ScriptLanguage language) { return "Python"; case eScriptLanguageLua: return "Lua"; + case eScriptLanguageJavaScript: + return "JavaScript"; case eScriptLanguageUnknown: return "Unknown"; } @@ -158,6 +160,8 @@ ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) { return eScriptLanguagePython; if (language.equals_insensitive(LanguageToString(eScriptLanguageLua))) return eScriptLanguageLua; + if (language.equals_insensitive(LanguageToString(eScriptLanguageJavaScript))) + return eScriptLanguageJavaScript; return eScriptLanguageUnknown; } diff --git a/lldb/source/Plugins/ScriptInterpreter/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/CMakeLists.txt index 4429b006173a7..ae74db4db31c0 100644 --- a/lldb/source/Plugins/ScriptInterpreter/CMakeLists.txt +++ b/lldb/source/Plugins/ScriptInterpreter/CMakeLists.txt @@ -8,3 +8,7 @@ endif() if (LLDB_ENABLE_LUA) add_subdirectory(Lua) endif() + +if (LLDB_ENABLE_JAVASCRIPT) + add_subdirectory(JavaScript) +endif() diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/JavaScript/CMakeLists.txt new file mode 100644 index 0000000000000..e7c5baffcaed5 --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/CMakeLists.txt @@ -0,0 +1,40 @@ +if(NOT LLDB_ENABLE_JAVASCRIPT) + return() +endif() + +find_package(V8) + +if(NOT V8_FOUND) + message(FATAL_ERROR "V8 JavaScript engine not found. JavaScript scripting will not be available.") + return() +endif() + +add_lldb_library(lldbPluginScriptInterpreterJavaScript PLUGIN + JavaScript.cpp + ScriptInterpreterJavaScript.cpp + ${CMAKE_BINARY_DIR}/tools/lldb/bindings/javascript/LLDBWrapJavaScript.cpp + + LINK_LIBS + lldbBreakpoint + lldbCore + lldbDataFormatters + lldbHost + lldbInterpreter + lldbTarget + lldbUtility + + LINK_COMPONENTS + Support + + CLANG_LIBS + clangBasic +) + +target_include_directories(lldbPluginScriptInterpreterJavaScript PUBLIC + ${V8_INCLUDE_DIR} +) + +# Link against V8 +if(V8_LIBRARIES) + target_link_libraries(lldbPluginScriptInterpreterJavaScript PRIVATE ${V8_LIBRARIES}) +endif() diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.cpp b/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.cpp new file mode 100644 index 0000000000000..a5c4074dabc3b --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.cpp @@ -0,0 +1,435 @@ +//===-- JavaScript.cpp ----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "JavaScript.h" +#include "lldb/API/SBDebugger.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Utility/FileSpec.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" + +#include +#include + +using namespace lldb_private; +using namespace lldb; + +// SWIG-generated init function (SWIGV8_INIT is a macro that expands to +// lldb_initialize) +extern "C" void lldb_initialize(v8::Local exports, + v8::Local module); + +// Static V8 platform (initialized once) +std::unique_ptr JavaScript::s_platform; +bool JavaScript::s_platform_initialized = false; + +// Helper to format and write output +static void +WriteFormattedOutput(const v8::FunctionCallbackInfo &args, + bool add_newline = true) { + v8::Isolate *isolate = args.GetIsolate(); + v8::HandleScope handle_scope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + JavaScript *js_instance = + static_cast(context->GetAlignedPointerFromEmbedderData(1)); + + std::string output; + for (int i = 0; i < args.Length(); i++) { + if (i > 0) + output += " "; + v8::String::Utf8Value str(isolate, args[i]); + output += *str; + } + if (add_newline) + output += "\n"; + + if (js_instance) { + js_instance->WriteOutput(output); + } else { + printf("%s", output.c_str()); + fflush(stdout); + } +} + +// Console.log implementation +static void ConsoleLog(const v8::FunctionCallbackInfo &args) { + WriteFormattedOutput(args, true); +} + +// Console.warn implementation +static void ConsoleWarn(const v8::FunctionCallbackInfo &args) { + WriteFormattedOutput(args, true); +} + +// Console.error implementation +static void ConsoleError(const v8::FunctionCallbackInfo &args) { + WriteFormattedOutput(args, true); +} + +void JavaScript::InitializePlatform() { + if (s_platform_initialized) + return; + + v8::V8::InitializeICUDefaultLocation(""); + v8::V8::InitializeExternalStartupData(""); + s_platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(s_platform.get()); + v8::V8::Initialize(); + + s_platform_initialized = true; +} + +JavaScript::JavaScript(lldb::FileSP output_file) + : m_stdout(stdout), m_stderr(stderr), m_output_file(output_file) { + InitializePlatform(); + + // Create isolate + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = + v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + m_isolate = v8::Isolate::New(create_params); + + // Create context + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + + v8::Local context = v8::Context::New(m_isolate); + m_context = new v8::Global(m_isolate, context); + + // Initialize SWIG bindings + v8::Context::Scope context_scope(context); + v8::Local lldb_module = v8::Object::New(m_isolate); + v8::Local empty_module = v8::Object::New(m_isolate); + + lldb_initialize(lldb_module, empty_module); + + context->Global() + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "lldb").ToLocalChecked(), + lldb_module) + .Check(); + + v8::Local console_obj = v8::Object::New(m_isolate); + console_obj + ->Set(context, v8::String::NewFromUtf8(m_isolate, "log").ToLocalChecked(), + v8::Function::New(context, ConsoleLog).ToLocalChecked()) + .Check(); + console_obj + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "warn").ToLocalChecked(), + v8::Function::New(context, ConsoleWarn).ToLocalChecked()) + .Check(); + console_obj + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "error").ToLocalChecked(), + v8::Function::New(context, ConsoleError).ToLocalChecked()) + .Check(); + context->Global() + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "console").ToLocalChecked(), + console_obj) + .Check(); + + context->SetAlignedPointerInEmbedderData(1, this); +} + +JavaScript::~JavaScript() { + // Clear all callbacks + for (auto &pair : m_breakpoint_callbacks) { + pair.second.Reset(); + } + m_breakpoint_callbacks.clear(); + + for (auto &pair : m_watchpoint_callbacks) { + pair.second.Reset(); + } + m_watchpoint_callbacks.clear(); + + if (m_context) { + m_context->Reset(); + delete m_context; + } + if (m_isolate) { + m_isolate->Dispose(); + } +} + +llvm::Error JavaScript::Run(llvm::StringRef code) { + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + v8::Local context = m_context->Get(m_isolate); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(m_isolate); + + // Compile + v8::Local source = + v8::String::NewFromUtf8(m_isolate, code.data(), + v8::NewStringType::kNormal, code.size()) + .ToLocalChecked(); + + v8::Local script; + if (!v8::Script::Compile(context, source).ToLocal(&script)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + return llvm::make_error( + llvm::formatv("Compilation error: {0}\n", *error), + llvm::inconvertibleErrorCode()); + } + + // Run + v8::Local result; + if (!script->Run(context).ToLocal(&result)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + return llvm::make_error( + llvm::formatv("Runtime error: {0}\n", *error), + llvm::inconvertibleErrorCode()); + } + + // Print the result if it's not undefined (REPL behavior) + if (!result->IsUndefined()) { + v8::String::Utf8Value result_str(m_isolate, result); + WriteOutput(std::string(*result_str) + "\n"); + } + + return llvm::Error::success(); +} + +llvm::Error JavaScript::LoadModule(llvm::StringRef filename) { + const FileSpec file(filename); + if (!FileSystem::Instance().Exists(file)) { + return llvm::make_error("File not found", + llvm::inconvertibleErrorCode()); + } + + if (file.GetFileNameExtension() != ".js") { + return llvm::make_error( + "Invalid extension (expected .js)", llvm::inconvertibleErrorCode()); + } + + // Read file using llvm MemoryBuffer + auto buffer_or_error = llvm::MemoryBuffer::getFile(file.GetPath()); + if (!buffer_or_error) { + return llvm::make_error( + llvm::formatv("Failed to read file: {0}", + buffer_or_error.getError().message()), + llvm::inconvertibleErrorCode()); + } + + std::unique_ptr buffer = std::move(*buffer_or_error); + llvm::StringRef contents = buffer->getBuffer(); + + if (contents.empty()) { + return llvm::make_error("Empty file", + llvm::inconvertibleErrorCode()); + } + + return Run(contents); +} + +llvm::Error JavaScript::CheckSyntax(llvm::StringRef code) { + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + v8::Local context = m_context->Get(m_isolate); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(m_isolate); + + v8::Local source = + v8::String::NewFromUtf8(m_isolate, code.data(), + v8::NewStringType::kNormal, code.size()) + .ToLocalChecked(); + + v8::Local script; + if (!v8::Script::Compile(context, source).ToLocal(&script)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + return llvm::make_error( + llvm::formatv("Syntax error: {0}\n", *error), + llvm::inconvertibleErrorCode()); + } + + return llvm::Error::success(); +} + +llvm::Error JavaScript::ChangeIO(FILE *out, FILE *err) { + m_stdout = out; + m_stderr = err; + return llvm::Error::success(); +} + +llvm::Error +JavaScript::RegisterBreakpointCallback(void *baton, + const char *command_body_text) { + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + v8::Local context = m_context->Get(m_isolate); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(m_isolate); + + v8::Local source = + v8::String::NewFromUtf8(m_isolate, command_body_text, + v8::NewStringType::kNormal, + strlen(command_body_text)) + .ToLocalChecked(); + + v8::Local script; + if (!v8::Script::Compile(context, source).ToLocal(&script)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + return llvm::make_error( + llvm::formatv("Failed to compile callback: {0}", *error), + llvm::inconvertibleErrorCode()); + } + + v8::Local result; + if (!script->Run(context).ToLocal(&result)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + return llvm::make_error( + llvm::formatv("Failed to evaluate callback: {0}", *error), + llvm::inconvertibleErrorCode()); + } + + if (!result->IsFunction()) { + return llvm::make_error( + "Breakpoint callback must be a JavaScript function", + llvm::inconvertibleErrorCode()); + } + + // Store the function in our map + v8::Local callback = result.As(); + m_breakpoint_callbacks[baton] = v8::Global(m_isolate, callback); + + return llvm::Error::success(); +} + +llvm::Expected +JavaScript::CallBreakpointCallback(void *baton, + lldb::StackFrameSP stop_frame_sp, + lldb::BreakpointLocationSP bp_loc_sp, + StructuredData::ObjectSP extra_args_sp) { + auto it = m_breakpoint_callbacks.find(baton); + if (it == m_breakpoint_callbacks.end()) { + return llvm::make_error( + "No callback registered for this baton", + llvm::inconvertibleErrorCode()); + } + + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + v8::Local context = m_context->Get(m_isolate); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(m_isolate); + + v8::Local callback = it->second.Get(m_isolate); + + v8::Local args[2] = {v8::Null(m_isolate), v8::Null(m_isolate)}; + + v8::Local result; + if (!callback->Call(context, context->Global(), 2, args).ToLocal(&result)) { + v8::String::Utf8Value error(m_isolate, try_catch.Exception()); + WriteOutput( + llvm::formatv("Breakpoint callback error: {0}\n", *error).str()); + return false; + } + + bool should_stop = false; + if (result->IsBoolean()) { + should_stop = result->BooleanValue(m_isolate); + } + + return should_stop; +} + +llvm::Error +JavaScript::RegisterWatchpointCallback(void *baton, + const char *command_body_text) { + return llvm::make_error( + "Watchpoint callbacks not yet implemented", + llvm::inconvertibleErrorCode()); +} + +llvm::Expected JavaScript::CallWatchpointCallback( + void *baton, lldb::StackFrameSP stop_frame_sp, lldb::WatchpointSP wp_sp) { + return false; +} + +void JavaScript::SetOutputCallback(OutputCallback callback) { + m_output_callback = callback; +} + +void JavaScript::WriteOutput(const std::string &text) { + if (m_output_callback) { + m_output_callback(text); + } else if (m_output_file && m_output_file->IsValid()) { + m_output_file->Printf("%s", text.c_str()); + m_output_file->Flush(); + } else { + printf("%s", text.c_str()); + fflush(stdout); + } +} + +void JavaScript::SetDebugger(lldb::DebuggerSP debugger_sp) { + m_debugger = debugger_sp; + + if (!debugger_sp) + return; + + v8::Isolate::Scope isolate_scope(m_isolate); + v8::HandleScope handle_scope(m_isolate); + v8::Local context = m_context->Get(m_isolate); + v8::Context::Scope context_scope(context); + + v8::Local lldb_val; + if (!context->Global() + ->Get(context, + v8::String::NewFromUtf8(m_isolate, "lldb").ToLocalChecked()) + .ToLocal(&lldb_val) || + !lldb_val->IsObject()) + return; + + v8::Local lldb_obj = lldb_val.As(); + + std::string js_code = llvm::formatv("lldb.SBDebugger.FindDebuggerWithID({0})", + debugger_sp->GetID()) + .str(); + + v8::TryCatch try_catch(m_isolate); + v8::Local source = + v8::String::NewFromUtf8(m_isolate, js_code.c_str(), + v8::NewStringType::kNormal, js_code.length()) + .ToLocalChecked(); + + v8::Local script; + if (!v8::Script::Compile(context, source).ToLocal(&script)) { + lldb_obj + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "debugger").ToLocalChecked(), + v8::Null(m_isolate)) + .Check(); + return; + } + + v8::Local debugger_obj; + if (!script->Run(context).ToLocal(&debugger_obj)) { + lldb_obj + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "debugger").ToLocalChecked(), + v8::Null(m_isolate)) + .Check(); + return; + } + + lldb_obj + ->Set(context, + v8::String::NewFromUtf8(m_isolate, "debugger").ToLocalChecked(), + debugger_obj) + .Check(); +} diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.h b/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.h new file mode 100644 index 0000000000000..ed13034e33969 --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/JavaScript.h @@ -0,0 +1,107 @@ +//===-- JavaScript.h --------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_JAVASCRIPT_H +#define LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_JAVASCRIPT_H + +#include "lldb/Utility/StructuredData.h" +#include "lldb/lldb-forward.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include +#include + +// Forward declare V8 types to avoid including V8 headers here +namespace v8 { +class Isolate; +template class Global; +class Context; +class Platform; +class Function; +} // namespace v8 + +namespace lldb_private { + +class JavaScript { +public: + JavaScript(lldb::FileSP output_file = nullptr); + ~JavaScript(); + + // Execute JavaScript code + llvm::Error Run(llvm::StringRef code); + + // Set callback for output (used by console.log) + using OutputCallback = std::function; + void SetOutputCallback(OutputCallback callback); + + // Load and execute a JavaScript module + llvm::Error LoadModule(llvm::StringRef filename); + + // Check syntax without executing + llvm::Error CheckSyntax(llvm::StringRef code); + + // Change IO streams + llvm::Error ChangeIO(FILE *out, FILE *err); + + // Breakpoint callback support + llvm::Error RegisterBreakpointCallback(void *baton, + const char *command_body_text); + + llvm::Expected + CallBreakpointCallback(void *baton, lldb::StackFrameSP stop_frame_sp, + lldb::BreakpointLocationSP bp_loc_sp, + StructuredData::ObjectSP extra_args_sp); + + // Watchpoint callback support + llvm::Error RegisterWatchpointCallback(void *baton, + const char *command_body_text); + + llvm::Expected CallWatchpointCallback(void *baton, + lldb::StackFrameSP stop_frame_sp, + lldb::WatchpointSP wp_sp); + + // Get the V8 isolate (for advanced usage) + v8::Isolate *GetIsolate() { return m_isolate; } + + // Get the output file (for console.log implementation) + lldb::FileSP GetOutputFile() { return m_output_file; } + + // Set the output file (for routing console.log to the correct stream) + void SetOutputFile(lldb::FileSP output_file) { m_output_file = output_file; } + + // Write output (used by console.log) + void WriteOutput(const std::string &text); + + // Set the debugger instance (exposes lldb.debugger to scripts) + void SetDebugger(lldb::DebuggerSP debugger_sp); + +private: + static std::unique_ptr s_platform; + static bool s_platform_initialized; + + v8::Isolate *m_isolate; + v8::Global *m_context; + + FILE *m_stdout; + FILE *m_stderr; + lldb::FileSP m_output_file; + OutputCallback m_output_callback; + + // Map from baton pointer to JavaScript callback function + std::map> m_breakpoint_callbacks; + std::map> m_watchpoint_callbacks; + + lldb::DebuggerSP m_debugger; + + // Initialize V8 platform (called once) + static void InitializePlatform(); +}; + +} // namespace lldb_private + +#endif // LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_JAVASCRIPT_H diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/SWIGJavaScriptBridge.h b/lldb/source/Plugins/ScriptInterpreter/JavaScript/SWIGJavaScriptBridge.h new file mode 100644 index 0000000000000..8e2a8579a70b5 --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/SWIGJavaScriptBridge.h @@ -0,0 +1,51 @@ +//===-- SWIGJavaScriptBridge.h --------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SWIGJAVASCRIPTBRIDGE_H +#define LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SWIGJAVASCRIPTBRIDGE_H + +#include "lldb/lldb-forward.h" +#include "llvm/Support/Error.h" + +namespace lldb_private { +class StructuredDataImpl; +} // namespace lldb_private + +namespace v8 { +class Isolate; +} // namespace v8 + +// This will be implemented by SWIG-generated code +extern "C" { +void init_lldb(v8::Isolate *isolate); +} + +namespace javascript { + +// Bridge functions for calling LLDB from JavaScript +// These will be generated/implemented by SWIG +namespace SWIGBridge { + +// TODO: Implement bridge functions +// These are similar to LuaBridge functions but for JavaScript/V8 + +llvm::Expected LLDBSwigJavaScriptBreakpointCallbackFunction( + v8::Isolate *isolate, lldb::StackFrameSP stop_frame_sp, + lldb::BreakpointLocationSP bp_loc_sp, + const lldb_private::StructuredDataImpl &extra_args_impl); + +llvm::Expected +LLDBSwigJavaScriptWatchpointCallbackFunction(v8::Isolate *isolate, + lldb::StackFrameSP stop_frame_sp, + lldb::WatchpointSP wp_sp); + +} // namespace SWIGBridge + +} // namespace javascript + +#endif // LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SWIGJAVASCRIPTBRIDGE_H diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.cpp b/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.cpp new file mode 100644 index 0000000000000..78f9d9faa502f --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.cpp @@ -0,0 +1,278 @@ +//===-- ScriptInterpreterJavaScript.cpp ----------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ScriptInterpreterJavaScript.h" +#include "JavaScript.h" + +#include "lldb/Breakpoint/StoppointCallbackContext.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/IOHandler.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Interpreter/CommandReturnObject.h" +#include "lldb/Target/ExecutionContext.h" +#include "lldb/Utility/Stream.h" +#include "lldb/Utility/StringList.h" +#include "lldb/Utility/Timer.h" +#include "llvm/Support/FormatVariadic.h" + +using namespace lldb; +using namespace lldb_private; + +LLDB_PLUGIN_DEFINE(ScriptInterpreterJavaScript) + +// IOHandler for JavaScript REPL +class IOHandlerJavaScriptInterpreter : public IOHandlerDelegate, + public IOHandlerEditline { +public: + IOHandlerJavaScriptInterpreter( + Debugger &debugger, ScriptInterpreterJavaScript &script_interpreter) + : IOHandlerEditline(debugger, IOHandler::Type::Other, "javascript", + "> ", // Prompt + llvm::StringRef(), // No continuation prompt + false, // Single-line for now + debugger.GetUseColor(), 0, *this), + m_script_interpreter(script_interpreter) { + llvm::cantFail(m_script_interpreter.EnterSession(debugger.GetID())); + } + + ~IOHandlerJavaScriptInterpreter() override { + llvm::cantFail(m_script_interpreter.LeaveSession()); + } + + void IOHandlerInputComplete(IOHandler &io_handler, + std::string &data) override { + if (data == "quit" || data == "exit") { + io_handler.SetIsDone(true); + return; + } + + // Execute the JavaScript code + llvm::Error error = m_script_interpreter.GetJavaScript().Run(data); + + if (error) { + // Print error + if (LockableStreamFileSP error_sp = io_handler.GetErrorStreamFileSP()) { + LockedStreamFile locked_stream = error_sp->Lock(); + locked_stream << "error: " << llvm::toString(std::move(error)) << "\n"; + } + } + } + +private: + ScriptInterpreterJavaScript &m_script_interpreter; +}; + +ScriptInterpreterJavaScript::ScriptInterpreterJavaScript(Debugger &debugger) + : ScriptInterpreter(debugger, eScriptLanguageJavaScript), + m_javascript(std::make_unique(debugger.GetOutputFileSP())) {} + +ScriptInterpreterJavaScript::~ScriptInterpreterJavaScript() = default; + +bool ScriptInterpreterJavaScript::ExecuteOneLine( + llvm::StringRef command, CommandReturnObject *result, + const ExecuteScriptOptions &options) { + if (command.empty()) { + if (result) + result->AppendError("Empty command string\n"); + return false; + } + + // Set output callback to write console.log output to the result stream + if (result) { + m_javascript->SetOutputCallback([result](const std::string &text) { + result->GetOutputStream().Printf("%s", text.c_str()); + }); + } + + llvm::Error error = m_javascript->Run(command); + + // Clear the callback after execution + m_javascript->SetOutputCallback(nullptr); + + if (error) { + if (result) + result->AppendError(llvm::toString(std::move(error))); + return false; + } + + if (result) + result->SetStatus(eReturnStatusSuccessFinishResult); + return true; +} + +void ScriptInterpreterJavaScript::ExecuteInterpreterLoop() { + LLDB_SCOPED_TIMER(); + + if (!m_debugger.GetInputFile().IsValid()) + return; + + IOHandlerSP io_handler_sp( + new IOHandlerJavaScriptInterpreter(m_debugger, *this)); + m_debugger.RunIOHandlerAsync(io_handler_sp); +} + +bool ScriptInterpreterJavaScript::LoadScriptingModule( + const char *filename, const LoadScriptOptions &options, + lldb_private::Status &error, StructuredData::ObjectSP *module_sp, + FileSpec extra_search_dir, lldb::TargetSP loaded_into_target_sp) { + + if (!filename || filename[0] == '\0') { + error = Status::FromErrorString("Empty filename"); + return false; + } + + llvm::Error session_error = EnterSession(m_debugger.GetID()); + if (session_error) { + error = Status::FromErrorString( + llvm::toString(std::move(session_error)).c_str()); + return false; + } + + llvm::Error load_error = m_javascript->LoadModule(filename); + if (load_error) { + error = + Status::FromErrorString(llvm::toString(std::move(load_error)).c_str()); + return false; + } + + return true; +} + +StructuredData::DictionarySP ScriptInterpreterJavaScript::GetInterpreterInfo() { + auto info_dict = std::make_shared(); + info_dict->AddStringItem("language", "javascript"); + info_dict->AddStringItem("version", "ES2020+ (V8)"); + return info_dict; +} + +void ScriptInterpreterJavaScript::Initialize() { + static llvm::once_flag g_once_flag; + llvm::call_once(g_once_flag, []() { + PluginManager::RegisterPlugin( + GetPluginNameStatic(), GetPluginDescriptionStatic(), + lldb::eScriptLanguageJavaScript, CreateInstance); + }); +} + +void ScriptInterpreterJavaScript::Terminate() { + PluginManager::UnregisterPlugin(CreateInstance); +} + +lldb::ScriptInterpreterSP +ScriptInterpreterJavaScript::CreateInstance(Debugger &debugger) { + return std::make_shared(debugger); +} + +llvm::StringRef ScriptInterpreterJavaScript::GetPluginDescriptionStatic() { + return "JavaScript script interpreter"; +} + +JavaScript &ScriptInterpreterJavaScript::GetJavaScript() { + return *m_javascript; +} + +llvm::Error +ScriptInterpreterJavaScript::EnterSession(lldb::user_id_t debugger_id) { + if (m_session_is_active) + return llvm::Error::success(); + + m_javascript->SetDebugger(m_debugger.shared_from_this()); + m_session_is_active = true; + return llvm::Error::success(); +} + +llvm::Error ScriptInterpreterJavaScript::LeaveSession() { + if (!m_session_is_active) + return llvm::Error::success(); + + m_session_is_active = false; + return llvm::Error::success(); +} + +void ScriptInterpreterJavaScript::CollectDataForBreakpointCommandCallback( + std::vector> &bp_options_vec, + CommandReturnObject &result) { + result.AppendError("Breakpoint callbacks not yet implemented for JavaScript"); +} + +void ScriptInterpreterJavaScript::CollectDataForWatchpointCommandCallback( + WatchpointOptions *wp_options, CommandReturnObject &result) { + result.AppendError("Watchpoint callbacks not yet implemented for JavaScript"); +} + +Status ScriptInterpreterJavaScript::SetBreakpointCommandCallback( + BreakpointOptions &bp_options, const char *command_body_text, + bool is_callback) { + return Status::FromErrorString( + "Breakpoint callbacks not yet implemented for JavaScript"); +} + +void ScriptInterpreterJavaScript::SetWatchpointCommandCallback( + WatchpointOptions *wp_options, const char *command_body_text, + bool is_callback) { + // TODO: Implement +} + +Status ScriptInterpreterJavaScript::SetBreakpointCommandCallbackFunction( + BreakpointOptions &bp_options, const char *function_name, + StructuredData::ObjectSP extra_args_sp) { + const char *fmt_str = "({0})"; + std::string oneliner = llvm::formatv(fmt_str, function_name).str(); + + auto data_up = std::make_unique(extra_args_sp); + + llvm::Error err = + m_javascript->RegisterBreakpointCallback(data_up.get(), oneliner.c_str()); + if (err) + return Status::FromError(std::move(err)); + + auto baton_sp = + std::make_shared(std::move(data_up)); + bp_options.SetCallback( + ScriptInterpreterJavaScript::BreakpointCallbackFunction, baton_sp); + + return Status(); +} + +bool ScriptInterpreterJavaScript::BreakpointCallbackFunction( + void *baton, StoppointCallbackContext *context, lldb::user_id_t break_id, + lldb::user_id_t break_loc_id) { + + ExecutionContext exe_ctx(context->exe_ctx_ref); + Target *target = exe_ctx.GetTargetPtr(); + if (!target) + return true; + + StackFrameSP stop_frame_sp(exe_ctx.GetFrameSP()); + BreakpointSP breakpoint_sp = target->GetBreakpointByID(break_id); + BreakpointLocationSP bp_loc_sp(breakpoint_sp->FindLocationByID(break_loc_id)); + + Debugger &debugger = target->GetDebugger(); + ScriptInterpreterJavaScript *js_interpreter = + static_cast( + debugger.GetScriptInterpreter(true, eScriptLanguageJavaScript)); + JavaScript &js = js_interpreter->GetJavaScript(); + + CommandDataJavaScript *bp_option_data = + static_cast(baton); + llvm::Expected BoolOrErr = + js.CallBreakpointCallback(baton, stop_frame_sp, bp_loc_sp, + bp_option_data->m_extra_args.GetObjectSP()); + if (llvm::Error E = BoolOrErr.takeError()) { + llvm::consumeError(std::move(E)); + return true; + } + + return *BoolOrErr; +} + +bool ScriptInterpreterJavaScript::WatchpointCallbackFunction( + void * /*baton*/, StoppointCallbackContext * /*context*/, + lldb::user_id_t /*watch_id*/) { + return false; +} diff --git a/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.h b/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.h new file mode 100644 index 0000000000000..40555d6c0fdca --- /dev/null +++ b/lldb/source/Plugins/ScriptInterpreter/JavaScript/ScriptInterpreterJavaScript.h @@ -0,0 +1,121 @@ +//===-- ScriptInterpreterJavaScript.h ---------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SCRIPTINTERPRETERJAVASCRIPT_H +#define LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SCRIPTINTERPRETERJAVASCRIPT_H + +#include +#include + +#include "lldb/Breakpoint/WatchpointOptions.h" +#include "lldb/Core/StructuredDataImpl.h" +#include "lldb/Interpreter/ScriptInterpreter.h" +#include "lldb/Utility/Status.h" +#include "lldb/lldb-enumerations.h" + +namespace v8 { +class Isolate; +template class Global; +class Context; +class Platform; +} // namespace v8 + +namespace lldb_private { + +class JavaScript; + +class ScriptInterpreterJavaScript : public ScriptInterpreter { +public: + class CommandDataJavaScript : public BreakpointOptions::CommandData { + public: + CommandDataJavaScript() : BreakpointOptions::CommandData() { + interpreter = lldb::eScriptLanguageJavaScript; + } + CommandDataJavaScript(StructuredData::ObjectSP extra_args_sp) + : BreakpointOptions::CommandData(), + m_extra_args(std::move(extra_args_sp)) { + interpreter = lldb::eScriptLanguageJavaScript; + } + StructuredDataImpl m_extra_args; + }; + + ScriptInterpreterJavaScript(Debugger &debugger); + + ~ScriptInterpreterJavaScript() override; + + bool ExecuteOneLine( + llvm::StringRef command, CommandReturnObject *result, + const ExecuteScriptOptions &options = ExecuteScriptOptions()) override; + + void ExecuteInterpreterLoop() override; + + bool LoadScriptingModule(const char *filename, + const LoadScriptOptions &options, + lldb_private::Status &error, + StructuredData::ObjectSP *module_sp = nullptr, + FileSpec extra_search_dir = {}, + lldb::TargetSP loaded_into_target_sp = {}) override; + + StructuredData::DictionarySP GetInterpreterInfo() override; + + // Static Functions + static void Initialize(); + + static void Terminate(); + + static lldb::ScriptInterpreterSP CreateInstance(Debugger &debugger); + + static llvm::StringRef GetPluginNameStatic() { return "script-javascript"; } + + static llvm::StringRef GetPluginDescriptionStatic(); + + static bool BreakpointCallbackFunction(void *baton, + StoppointCallbackContext *context, + lldb::user_id_t break_id, + lldb::user_id_t break_loc_id); + + static bool WatchpointCallbackFunction(void *baton, + StoppointCallbackContext *context, + lldb::user_id_t watch_id); + + // PluginInterface protocol + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } + + JavaScript &GetJavaScript(); + + llvm::Error EnterSession(lldb::user_id_t debugger_id); + llvm::Error LeaveSession(); + + void CollectDataForBreakpointCommandCallback( + std::vector> &bp_options_vec, + CommandReturnObject &result) override; + + void + CollectDataForWatchpointCommandCallback(WatchpointOptions *wp_options, + CommandReturnObject &result) override; + + Status SetBreakpointCommandCallback(BreakpointOptions &bp_options, + const char *command_body_text, + bool is_callback) override; + + void SetWatchpointCommandCallback(WatchpointOptions *wp_options, + const char *command_body_text, + bool is_callback) override; + + Status SetBreakpointCommandCallbackFunction( + BreakpointOptions &bp_options, const char *function_name, + StructuredData::ObjectSP extra_args_sp) override; + +private: + std::unique_ptr m_javascript; + bool m_session_is_active = false; +}; + +} // namespace lldb_private + +#endif // LLVM_LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_JAVASCRIPT_SCRIPTINTERPRETERJAVASCRIPT_H