| //===-- JSONUtils.cpp -------------------------------------------*- 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 |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "JSONUtils.h" |
| #include "DAP.h" |
| #include "ExceptionBreakpoint.h" |
| #include "Protocol/ProtocolBase.h" |
| #include "Protocol/ProtocolRequests.h" |
| #include "lldb/API/SBAddress.h" |
| #include "lldb/API/SBDeclaration.h" |
| #include "lldb/API/SBError.h" |
| #include "lldb/API/SBFileSpec.h" |
| #include "lldb/API/SBLineEntry.h" |
| #include "lldb/API/SBStream.h" |
| #include "lldb/API/SBStringList.h" |
| #include "lldb/API/SBStructuredData.h" |
| #include "lldb/API/SBTarget.h" |
| #include "lldb/API/SBThread.h" |
| #include "lldb/API/SBType.h" |
| #include "lldb/API/SBValue.h" |
| #include "lldb/Host/PosixApi.h" // IWYU pragma: keep |
| #include "lldb/lldb-defines.h" |
| #include "lldb/lldb-enumerations.h" |
| #include "lldb/lldb-types.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/ADT/StringExtras.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/Compiler.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include "llvm/Support/JSON.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include <chrono> |
| #include <cstddef> |
| #include <optional> |
| #include <sstream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| namespace lldb_dap { |
| |
| void EmplaceSafeString(llvm::json::Object &obj, llvm::StringRef key, |
| llvm::StringRef str) { |
| if (LLVM_LIKELY(llvm::json::isUTF8(str))) |
| obj.try_emplace(key, str.str()); |
| else |
| obj.try_emplace(key, llvm::json::fixUTF8(str)); |
| } |
| |
| std::string EncodeMemoryReference(lldb::addr_t addr) { |
| return "0x" + llvm::utohexstr(addr); |
| } |
| |
| std::optional<lldb::addr_t> |
| DecodeMemoryReference(llvm::StringRef memoryReference) { |
| if (!memoryReference.starts_with("0x")) |
| return std::nullopt; |
| |
| lldb::addr_t addr; |
| if (memoryReference.consumeInteger(0, addr)) |
| return std::nullopt; |
| |
| return addr; |
| } |
| |
| bool DecodeMemoryReference(const llvm::json::Value &v, llvm::StringLiteral key, |
| lldb::addr_t &out, llvm::json::Path path, |
| bool required, bool allow_empty) { |
| const llvm::json::Object *v_obj = v.getAsObject(); |
| if (!v_obj) { |
| path.report("expected object"); |
| return false; |
| } |
| |
| const llvm::json::Value *mem_ref_value = v_obj->get(key); |
| if (!mem_ref_value) { |
| if (!required) |
| return true; |
| |
| path.field(key).report("missing value"); |
| return false; |
| } |
| |
| const std::optional<llvm::StringRef> mem_ref_str = |
| mem_ref_value->getAsString(); |
| if (!mem_ref_str) { |
| path.field(key).report("expected string"); |
| return false; |
| } |
| |
| if (allow_empty && mem_ref_str->empty()) { |
| out = LLDB_INVALID_ADDRESS; |
| return true; |
| } |
| |
| const std::optional<lldb::addr_t> addr_opt = |
| DecodeMemoryReference(*mem_ref_str); |
| if (!addr_opt) { |
| path.field(key).report("malformed memory reference"); |
| return false; |
| } |
| |
| out = *addr_opt; |
| return true; |
| } |
| |
| static bool IsClassStructOrUnionType(lldb::SBType t) { |
| return (t.GetTypeClass() & (lldb::eTypeClassUnion | lldb::eTypeClassStruct | |
| lldb::eTypeClassArray)) != 0; |
| } |
| |
| /// Create a short summary for a container that contains the summary of its |
| /// first children, so that the user can get a glimpse of its contents at a |
| /// glance. |
| static std::optional<std::string> |
| TryCreateAutoSummaryForContainer(lldb::SBValue &v) { |
| if (!v.MightHaveChildren()) |
| return std::nullopt; |
| /// As this operation can be potentially slow, we limit the total time spent |
| /// fetching children to a few ms. |
| const auto max_evaluation_time = std::chrono::milliseconds(10); |
| /// We don't want to generate a extremely long summary string, so we limit its |
| /// length. |
| const size_t max_length = 32; |
| |
| auto start = std::chrono::steady_clock::now(); |
| std::string summary; |
| llvm::raw_string_ostream os(summary); |
| os << "{"; |
| |
| llvm::StringRef separator = ""; |
| |
| for (size_t i = 0, e = v.GetNumChildren(); i < e; ++i) { |
| // If we reached the time limit or exceeded the number of characters, we |
| // dump `...` to signal that there are more elements in the collection. |
| if (summary.size() > max_length || |
| (std::chrono::steady_clock::now() - start) > max_evaluation_time) { |
| os << separator << "..."; |
| break; |
| } |
| lldb::SBValue child = v.GetChildAtIndex(i); |
| |
| if (llvm::StringRef name = child.GetName(); !name.empty()) { |
| llvm::StringRef desc; |
| if (llvm::StringRef summary = child.GetSummary(); !summary.empty()) |
| desc = summary; |
| else if (llvm::StringRef value = child.GetValue(); !value.empty()) |
| desc = value; |
| else if (IsClassStructOrUnionType(child.GetType())) |
| desc = "{...}"; |
| else |
| continue; |
| |
| // If the child is an indexed entry, we don't show its index to save |
| // characters. |
| if (name.starts_with("[")) |
| os << separator << desc; |
| else |
| os << separator << name << ":" << desc; |
| separator = ", "; |
| } |
| } |
| os << "}"; |
| |
| if (summary == "{...}" || summary == "{}") |
| return std::nullopt; |
| return summary; |
| } |
| |
| /// Try to create a summary string for the given value that doesn't have a |
| /// summary of its own. |
| static std::optional<std::string> TryCreateAutoSummary(lldb::SBValue &value) { |
| // We use the dereferenced value for generating the summary. |
| if (value.GetType().IsPointerType() || value.GetType().IsReferenceType()) |
| value = value.Dereference(); |
| |
| // We only support auto summaries for containers. |
| return TryCreateAutoSummaryForContainer(value); |
| } |
| |
| void FillResponse(const llvm::json::Object &request, |
| llvm::json::Object &response) { |
| // Fill in all of the needed response fields to a "request" and set "success" |
| // to true by default. |
| response.try_emplace("type", "response"); |
| response.try_emplace("seq", protocol::kCalculateSeq); |
| EmplaceSafeString(response, "command", |
| request.getString("command").value_or("")); |
| const uint64_t seq = GetInteger<uint64_t>(request, "seq").value_or(0); |
| response.try_emplace("request_seq", seq); |
| response.try_emplace("success", true); |
| } |
| |
| // "Event": { |
| // "allOf": [ { "$ref": "#/definitions/ProtocolMessage" }, { |
| // "type": "object", |
| // "description": "Server-initiated event.", |
| // "properties": { |
| // "type": { |
| // "type": "string", |
| // "enum": [ "event" ] |
| // }, |
| // "event": { |
| // "type": "string", |
| // "description": "Type of event." |
| // }, |
| // "body": { |
| // "type": [ "array", "boolean", "integer", "null", "number" , |
| // "object", "string" ], |
| // "description": "Event-specific information." |
| // } |
| // }, |
| // "required": [ "type", "event" ] |
| // }] |
| // }, |
| // "ProtocolMessage": { |
| // "type": "object", |
| // "description": "Base class of requests, responses, and events.", |
| // "properties": { |
| // "seq": { |
| // "type": "integer", |
| // "description": "Sequence number." |
| // }, |
| // "type": { |
| // "type": "string", |
| // "description": "Message type.", |
| // "_enum": [ "request", "response", "event" ] |
| // } |
| // }, |
| // "required": [ "seq", "type" ] |
| // } |
| llvm::json::Object CreateEventObject(const llvm::StringRef event_name) { |
| llvm::json::Object event; |
| event.try_emplace("seq", protocol::kCalculateSeq); |
| event.try_emplace("type", "event"); |
| EmplaceSafeString(event, "event", event_name); |
| return event; |
| } |
| |
| llvm::StringRef GetNonNullVariableName(lldb::SBValue &v) { |
| const llvm::StringRef name = v.GetName(); |
| return !name.empty() ? name : "(anonymous)"; |
| } |
| |
| std::string CreateUniqueVariableNameForDisplay(lldb::SBValue &v, |
| bool is_name_duplicated) { |
| std::string unique_name{}; |
| llvm::raw_string_ostream name_builder(unique_name); |
| name_builder << GetNonNullVariableName(v); |
| if (is_name_duplicated) { |
| const lldb::SBDeclaration declaration = v.GetDeclaration(); |
| const llvm::StringRef file_name = declaration.GetFileSpec().GetFilename(); |
| const uint32_t line = declaration.GetLine(); |
| |
| if (!file_name.empty() && line != 0 && line != LLDB_INVALID_LINE_NUMBER) |
| name_builder << llvm::formatv(" @ {}:{}", file_name, line); |
| else if (llvm::StringRef location = v.GetLocation(); !location.empty()) |
| name_builder << llvm::formatv(" @ {}", location); |
| } |
| return unique_name; |
| } |
| |
| VariableDescription::VariableDescription( |
| lldb::SBValue val, bool auto_variable_summaries, bool format_hex, |
| bool is_name_duplicated, std::optional<llvm::StringRef> custom_name) |
| : val(val) { |
| name = custom_name.value_or( |
| CreateUniqueVariableNameForDisplay(val, is_name_duplicated)); |
| |
| type_obj = val.GetType(); |
| const llvm::StringRef type_name = type_obj.GetDisplayTypeName(); |
| display_type_name = type_name.empty() ? NO_TYPENAME : type_name; |
| |
| // Only format hex/default if there is no existing special format. |
| if (const lldb::Format current_format = val.GetFormat(); |
| current_format == lldb::eFormatDefault || |
| current_format == lldb::eFormatHex) { |
| |
| val.SetFormat(format_hex ? lldb::eFormatHex : lldb::eFormatDefault); |
| } |
| |
| llvm::raw_string_ostream os_display_value(display_value); |
| |
| if (lldb::SBError sb_error = val.GetError(); sb_error.Fail()) { |
| error = sb_error.GetCString(); |
| os_display_value << "<error: " << error << ">"; |
| } else { |
| value = val.GetValue(); |
| summary = val.GetSummary(); |
| if (summary.empty() && auto_variable_summaries) |
| auto_summary = TryCreateAutoSummary(val); |
| |
| llvm::StringRef display_summary = auto_summary ? *auto_summary : summary; |
| const bool has_summary = !display_summary.empty(); |
| |
| if (!value.empty()) { |
| os_display_value << value; |
| if (has_summary) |
| os_display_value << " " << display_summary; |
| } else if (has_summary) { |
| os_display_value << display_summary; |
| |
| } else if (!type_name.empty()) { |
| // As last resort, we print its type if available. |
| os_display_value << type_name; |
| } |
| } |
| |
| // Only include the evaluation name if the name is not empty. If the name is |
| // empty then 'GetExpressionPath' will return an empty string like 'foo.', |
| // which does not actually work in expression evaluation. |
| if (!llvm::StringRef{val.GetName()}.empty()) { |
| lldb::SBStream evaluateStream; |
| val.GetExpressionPath(evaluateStream); |
| evaluate_name = |
| llvm::StringRef{evaluateStream.GetData(), evaluateStream.GetSize()} |
| .str(); |
| } |
| } |
| |
| std::string VariableDescription::GetResult(protocol::EvaluateContext context) { |
| // In repl and clipboard contexts, the results can be displayed as multiple |
| // lines so more detailed descriptions can be returned. |
| if (context != protocol::eEvaluateContextRepl && |
| context != protocol::eEvaluateContextClipboard) |
| return display_value; |
| |
| if (!val.IsValid()) |
| return display_value; |
| |
| // Try the SBValue::GetDescription(), which may call into language runtime |
| // specific formatters (see ValueObjectPrinter). |
| lldb::SBStream stream; |
| if (context == protocol::eEvaluateContextRepl) |
| val.GetDescription(stream, lldb::eDescriptionLevelFull); |
| else |
| val.GetDescription(stream, lldb::eDescriptionLevelBrief); |
| llvm::StringRef description = stream.GetData(); |
| return description.trim().str(); |
| } |
| |
| bool ValuePointsToCode(lldb::SBValue v) { |
| lldb::SBType type = v.GetType(); |
| if (!type.GetPointeeType().IsFunctionType()) |
| return false; |
| |
| lldb::SBError error; |
| lldb::addr_t addr = v.GetData().GetAddress(error, 0); |
| lldb::SBLineEntry line_entry = |
| v.GetTarget().ResolveLoadAddress(addr).GetLineEntry(); |
| |
| return line_entry.IsValid(); |
| } |
| |
| int64_t PackLocation(int64_t var_ref, bool is_value_location) { |
| return var_ref << 1 | is_value_location; |
| } |
| |
| std::pair<int64_t, bool> UnpackLocation(int64_t location_id) { |
| return std::pair{location_id >> 1, location_id & 1}; |
| } |
| |
| /// See |
| /// https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_RunInTerminal |
| llvm::json::Object CreateRunInTerminalReverseRequest( |
| llvm::StringRef program, const std::vector<protocol::String> &args, |
| const llvm::StringMap<protocol::String> &env, llvm::StringRef cwd, |
| llvm::StringRef comm_file, lldb::pid_t debugger_pid, |
| const std::vector<std::optional<protocol::String>> &stdio, bool external) { |
| llvm::json::Object run_in_terminal_args; |
| if (external) { |
| // This indicates the IDE to open an external terminal window. |
| run_in_terminal_args.try_emplace("kind", "external"); |
| } else { |
| // This indicates the IDE to open an embedded terminal, instead of opening |
| // the terminal in a new window. |
| run_in_terminal_args.try_emplace("kind", "integrated"); |
| } |
| // The program path must be the first entry in the "args" field |
| std::vector<std::string> req_args = {DAP::debug_adapter_path.str(), |
| "--comm-file", comm_file.str()}; |
| if (debugger_pid != LLDB_INVALID_PROCESS_ID) { |
| req_args.push_back("--debugger-pid"); |
| req_args.push_back(std::to_string(debugger_pid)); |
| } |
| |
| if (!stdio.empty()) { |
| req_args.emplace_back("--stdio"); |
| |
| std::stringstream ss; |
| std::string_view delimiter; |
| for (const std::optional<protocol::String> &file : stdio) { |
| #ifdef _WIN32 |
| ss << std::exchange(delimiter, ";"); |
| #else |
| ss << std::exchange(delimiter, ":"); |
| #endif |
| if (file) |
| ss << file->str(); |
| } |
| req_args.push_back(ss.str()); |
| } |
| |
| // WARNING: Any argument added after `launch-target` is passed to to the |
| // target. |
| req_args.emplace_back("--launch-target"); |
| req_args.push_back(program.str()); |
| req_args.insert(req_args.end(), args.begin(), args.end()); |
| run_in_terminal_args.try_emplace("args", req_args); |
| |
| if (!cwd.empty()) |
| run_in_terminal_args.try_emplace("cwd", cwd); |
| |
| if (!env.empty()) { |
| llvm::json::Object env_json; |
| for (const auto &kv : env) { |
| if (!kv.first().empty()) |
| env_json.try_emplace(kv.first(), kv.second); |
| } |
| run_in_terminal_args.try_emplace("env", |
| llvm::json::Value(std::move(env_json))); |
| } |
| |
| return run_in_terminal_args; |
| } |
| |
| // Keep all the top level items from the statistics dump, except for the |
| // "modules" array. It can be huge and cause delay |
| // Array and dictionary value will return as <key, JSON string> pairs |
| static void FilterAndGetValueForKey(const lldb::SBStructuredData data, |
| const char *key, llvm::json::Object &out) { |
| lldb::SBStructuredData value = data.GetValueForKey(key); |
| std::string key_utf8 = llvm::json::fixUTF8(key); |
| if (llvm::StringRef(key) == "modules") |
| return; |
| switch (value.GetType()) { |
| case lldb::eStructuredDataTypeFloat: |
| out.try_emplace(key_utf8, value.GetFloatValue()); |
| break; |
| case lldb::eStructuredDataTypeUnsignedInteger: |
| out.try_emplace(key_utf8, value.GetIntegerValue((uint64_t)0)); |
| break; |
| case lldb::eStructuredDataTypeSignedInteger: |
| out.try_emplace(key_utf8, value.GetIntegerValue((int64_t)0)); |
| break; |
| case lldb::eStructuredDataTypeArray: { |
| lldb::SBStream contents; |
| value.GetAsJSON(contents); |
| out.try_emplace(key_utf8, llvm::json::fixUTF8(contents.GetData())); |
| } break; |
| case lldb::eStructuredDataTypeBoolean: |
| out.try_emplace(key_utf8, value.GetBooleanValue()); |
| break; |
| case lldb::eStructuredDataTypeString: { |
| // Get the string size before reading |
| const size_t str_length = value.GetStringValue(nullptr, 0); |
| std::string str(str_length + 1, 0); |
| value.GetStringValue(&str[0], str_length); |
| out.try_emplace(key_utf8, llvm::json::fixUTF8(str)); |
| } break; |
| case lldb::eStructuredDataTypeDictionary: { |
| lldb::SBStream contents; |
| value.GetAsJSON(contents); |
| out.try_emplace(key_utf8, llvm::json::fixUTF8(contents.GetData())); |
| } break; |
| case lldb::eStructuredDataTypeNull: |
| case lldb::eStructuredDataTypeGeneric: |
| case lldb::eStructuredDataTypeInvalid: |
| break; |
| } |
| } |
| |
| static void addStatistic(lldb::SBTarget &target, llvm::json::Object &event) { |
| lldb::SBStructuredData statistics = target.GetStatistics(); |
| bool is_dictionary = |
| statistics.GetType() == lldb::eStructuredDataTypeDictionary; |
| if (!is_dictionary) |
| return; |
| llvm::json::Object stats_body; |
| |
| lldb::SBStringList keys; |
| if (!statistics.GetKeys(keys)) |
| return; |
| for (size_t i = 0; i < keys.GetSize(); i++) { |
| const char *key = keys.GetStringAtIndex(i); |
| FilterAndGetValueForKey(statistics, key, stats_body); |
| } |
| llvm::json::Object body{{"$__lldb_statistics", std::move(stats_body)}}; |
| event.try_emplace("body", std::move(body)); |
| } |
| |
| llvm::json::Object CreateTerminatedEventObject(lldb::SBTarget &target) { |
| llvm::json::Object event(CreateEventObject("terminated")); |
| addStatistic(target, event); |
| return event; |
| } |
| |
| llvm::json::Object CreateInitializedEventObject(lldb::SBTarget &target) { |
| llvm::json::Object event(CreateEventObject("initialized")); |
| addStatistic(target, event); |
| return event; |
| } |
| |
| std::string JSONToString(const llvm::json::Value &json) { |
| std::string data; |
| llvm::raw_string_ostream os(data); |
| os << json; |
| return data; |
| } |
| |
| } // namespace lldb_dap |