From f5a1f23562878a7d46cc719247ca4aa64b5efc17 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Fri, 14 Mar 2025 11:55:19 +1100 Subject: [PATCH 1/9] Added files from folders 'extensions/cxx_debugging' and 'extension-api' of commit 2f89fb20f4601f188569f991a98198f83322f2e2 in https://github.com/ChromeDevTools/devtools-frontend.git --- extension-api/ExtensionAPI.d.ts | 345 +++++++++ extensions/cxx_debugging/CMakeLists.txt | 168 +++++ .../e2e/resources/huge-source-file.cc | 29 + .../cxx_debugging/e2e/resources/pointers.cc | 40 + .../e2e/resources/scope-view-non-primitives.c | 30 + .../resources/scope-view-non-primitives.cpp | 49 ++ .../e2e/resources/scope-view-primitives.c | 19 + .../e2e/resources/stepping-with-state.c | 31 + .../e2e/resources/test_wasm_simd.c | 13 + .../cxx_debugging/e2e/resources/vector.cc | 14 + .../cxx_debugging/e2e/resources/wchar.cc | 22 + .../cxx_debugging/e2e/tests/cpp_eval.yaml | 37 + .../cxx_debugging/e2e/tests/pointer.yaml | 42 ++ .../e2e/tests/scope_view_non_primitives.yaml | 35 + .../tests/scope_view_non_primitives_cpp.yaml | 44 ++ .../e2e/tests/scope_view_primitives.yaml | 31 + .../e2e/tests/stepping_with_state.yaml | 64 ++ .../cxx_debugging/e2e/tests/string_view.yaml | 32 + .../cxx_debugging/e2e/tests/test_big_dwo.yaml | 21 + .../cxx_debugging/e2e/tests/test_loop.yaml | 34 + .../e2e/tests/test_wasm_simd.yaml | 42 ++ .../cxx_debugging/e2e/tests/vector.yaml | 26 + extensions/cxx_debugging/e2e/tests/wchar.yaml | 52 ++ extensions/cxx_debugging/lib/ApiContext.cc | 627 ++++++++++++++++ extensions/cxx_debugging/lib/ApiContext.h | 119 +++ extensions/cxx_debugging/lib/CMakeLists.txt | 53 ++ extensions/cxx_debugging/lib/Expressions.cc | 303 ++++++++ extensions/cxx_debugging/lib/Expressions.h | 53 ++ extensions/cxx_debugging/lib/Variables.cc | 101 +++ extensions/cxx_debugging/lib/Variables.h | 65 ++ extensions/cxx_debugging/lib/WasmModule.cc | 573 +++++++++++++++ extensions/cxx_debugging/lib/WasmModule.h | 126 ++++ .../cxx_debugging/lib/WasmVendorPlugins.cc | 172 +++++ .../cxx_debugging/lib/WasmVendorPlugins.h | 329 +++++++++ extensions/cxx_debugging/lib/api.h | 593 +++++++++++++++ extensions/cxx_debugging/src/CMakeLists.txt | 158 ++++ .../cxx_debugging/src/CustomFormatters.ts | 690 ++++++++++++++++++ extensions/cxx_debugging/src/DWARFSymbols.ts | 615 ++++++++++++++++ .../cxx_debugging/src/DevToolsPluginWorker.ts | 120 +++ extensions/cxx_debugging/src/Formatters.ts | 290 ++++++++ .../cxx_debugging/src/MEMFSResourceLoader.ts | 102 +++ .../cxx_debugging/src/ModuleConfiguration.ts | 117 +++ .../cxx_debugging/src/SymbolsBackend.cc | 262 +++++++ .../cxx_debugging/src/SymbolsBackend.d.ts | 179 +++++ extensions/cxx_debugging/src/WasmTypes.ts | 119 +++ extensions/cxx_debugging/src/WorkerRPC.ts | 159 ++++ extensions/cxx_debugging/tests/CMakeLists.txt | 146 ++++ .../tests/CustomFormatters_test.ts | 307 ++++++++ .../cxx_debugging/tests/Formatters_test.ts | 284 +++++++ .../cxx_debugging/tests/Interpreter_test.ts | 219 ++++++ .../cxx_debugging/tests/LLDBEvalExtensions.h | 154 ++++ .../cxx_debugging/tests/LLDBEvalTests.d.ts | 24 + .../tests/ModuleConfiguration_test.ts | 136 ++++ .../tests/SymbolsBackendTests.d.ts | 12 + .../tests/SymbolsBackend_test.ts | 39 + extensions/cxx_debugging/tests/TestUtils.ts | 250 +++++++ .../cxx_debugging/tests/WasmModule_test.cc | 332 +++++++++ .../cxx_debugging/tests/inputs/CMakeLists.txt | 141 ++++ .../cxx_debugging/tests/inputs/addr_index.s | 97 +++ .../cxx_debugging/tests/inputs/addresses.cc | 25 + .../cxx_debugging/tests/inputs/classstatic.s | 119 +++ .../cxx_debugging/tests/inputs/dw_opcodes.def | 66 ++ .../cxx_debugging/tests/inputs/embedded.s | 124 ++++ extensions/cxx_debugging/tests/inputs/enums.s | 376 ++++++++++ .../cxx_debugging/tests/inputs/externref.js | 22 + .../cxx_debugging/tests/inputs/externref.s | 207 ++++++ .../cxx_debugging/tests/inputs/globals.s | 443 +++++++++++ .../tests/inputs/hello-split-missing-dwo.s | 92 +++ .../cxx_debugging/tests/inputs/hello-split.s | 134 ++++ extensions/cxx_debugging/tests/inputs/hello.s | 70 ++ .../cxx_debugging/tests/inputs/helper.s | 130 ++++ .../cxx_debugging/tests/inputs/inline.s | 196 +++++ .../cxx_debugging/tests/inputs/namespaces.s | 207 ++++++ .../cxx_debugging/tests/inputs/shadowing.s | 121 +++ .../cxx_debugging/tests/inputs/split-dwarf.s | 126 ++++ .../cxx_debugging/tests/inputs/string_view.cc | 40 + .../tests/inputs/windows_paths.s | 70 ++ .../cxx_debugging/third_party/.clang-format | 1 + .../cxx_debugging/third_party/.gitignore | 2 + 79 files changed, 11857 insertions(+) create mode 100644 extension-api/ExtensionAPI.d.ts create mode 100644 extensions/cxx_debugging/CMakeLists.txt create mode 100644 extensions/cxx_debugging/e2e/resources/huge-source-file.cc create mode 100644 extensions/cxx_debugging/e2e/resources/pointers.cc create mode 100644 extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c create mode 100644 extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp create mode 100644 extensions/cxx_debugging/e2e/resources/scope-view-primitives.c create mode 100644 extensions/cxx_debugging/e2e/resources/stepping-with-state.c create mode 100644 extensions/cxx_debugging/e2e/resources/test_wasm_simd.c create mode 100644 extensions/cxx_debugging/e2e/resources/vector.cc create mode 100644 extensions/cxx_debugging/e2e/resources/wchar.cc create mode 100644 extensions/cxx_debugging/e2e/tests/cpp_eval.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/pointer.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/string_view.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/test_loop.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/vector.yaml create mode 100644 extensions/cxx_debugging/e2e/tests/wchar.yaml create mode 100644 extensions/cxx_debugging/lib/ApiContext.cc create mode 100644 extensions/cxx_debugging/lib/ApiContext.h create mode 100644 extensions/cxx_debugging/lib/CMakeLists.txt create mode 100644 extensions/cxx_debugging/lib/Expressions.cc create mode 100644 extensions/cxx_debugging/lib/Expressions.h create mode 100644 extensions/cxx_debugging/lib/Variables.cc create mode 100644 extensions/cxx_debugging/lib/Variables.h create mode 100644 extensions/cxx_debugging/lib/WasmModule.cc create mode 100644 extensions/cxx_debugging/lib/WasmModule.h create mode 100644 extensions/cxx_debugging/lib/WasmVendorPlugins.cc create mode 100644 extensions/cxx_debugging/lib/WasmVendorPlugins.h create mode 100644 extensions/cxx_debugging/lib/api.h create mode 100644 extensions/cxx_debugging/src/CMakeLists.txt create mode 100644 extensions/cxx_debugging/src/CustomFormatters.ts create mode 100644 extensions/cxx_debugging/src/DWARFSymbols.ts create mode 100644 extensions/cxx_debugging/src/DevToolsPluginWorker.ts create mode 100644 extensions/cxx_debugging/src/Formatters.ts create mode 100644 extensions/cxx_debugging/src/MEMFSResourceLoader.ts create mode 100644 extensions/cxx_debugging/src/ModuleConfiguration.ts create mode 100644 extensions/cxx_debugging/src/SymbolsBackend.cc create mode 100644 extensions/cxx_debugging/src/SymbolsBackend.d.ts create mode 100644 extensions/cxx_debugging/src/WasmTypes.ts create mode 100644 extensions/cxx_debugging/src/WorkerRPC.ts create mode 100644 extensions/cxx_debugging/tests/CMakeLists.txt create mode 100644 extensions/cxx_debugging/tests/CustomFormatters_test.ts create mode 100644 extensions/cxx_debugging/tests/Formatters_test.ts create mode 100644 extensions/cxx_debugging/tests/Interpreter_test.ts create mode 100644 extensions/cxx_debugging/tests/LLDBEvalExtensions.h create mode 100644 extensions/cxx_debugging/tests/LLDBEvalTests.d.ts create mode 100644 extensions/cxx_debugging/tests/ModuleConfiguration_test.ts create mode 100644 extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts create mode 100644 extensions/cxx_debugging/tests/SymbolsBackend_test.ts create mode 100644 extensions/cxx_debugging/tests/TestUtils.ts create mode 100644 extensions/cxx_debugging/tests/WasmModule_test.cc create mode 100644 extensions/cxx_debugging/tests/inputs/CMakeLists.txt create mode 100644 extensions/cxx_debugging/tests/inputs/addr_index.s create mode 100644 extensions/cxx_debugging/tests/inputs/addresses.cc create mode 100644 extensions/cxx_debugging/tests/inputs/classstatic.s create mode 100644 extensions/cxx_debugging/tests/inputs/dw_opcodes.def create mode 100644 extensions/cxx_debugging/tests/inputs/embedded.s create mode 100644 extensions/cxx_debugging/tests/inputs/enums.s create mode 100644 extensions/cxx_debugging/tests/inputs/externref.js create mode 100644 extensions/cxx_debugging/tests/inputs/externref.s create mode 100644 extensions/cxx_debugging/tests/inputs/globals.s create mode 100644 extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s create mode 100644 extensions/cxx_debugging/tests/inputs/hello-split.s create mode 100644 extensions/cxx_debugging/tests/inputs/hello.s create mode 100644 extensions/cxx_debugging/tests/inputs/helper.s create mode 100644 extensions/cxx_debugging/tests/inputs/inline.s create mode 100644 extensions/cxx_debugging/tests/inputs/namespaces.s create mode 100644 extensions/cxx_debugging/tests/inputs/shadowing.s create mode 100644 extensions/cxx_debugging/tests/inputs/split-dwarf.s create mode 100644 extensions/cxx_debugging/tests/inputs/string_view.cc create mode 100644 extensions/cxx_debugging/tests/inputs/windows_paths.s create mode 100644 extensions/cxx_debugging/third_party/.clang-format create mode 100644 extensions/cxx_debugging/third_party/.gitignore diff --git a/extension-api/ExtensionAPI.d.ts b/extension-api/ExtensionAPI.d.ts new file mode 100644 index 0000000..c3ad135 --- /dev/null +++ b/extension-api/ExtensionAPI.d.ts @@ -0,0 +1,345 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export namespace Chrome { + export namespace DevTools { + export interface EventSink void> { + addListener(listener: ListenerT): void; + removeListener(listener: ListenerT): void; + } + + export interface Resource { + readonly url: string; + readonly type: string; + + getContent(callback: (content: string, encoding: string) => unknown): void; + setContent(content: string, commit: boolean, callback?: (error?: Object) => unknown): void; + /** + * Augments this resource's scopes information based on the list of {@link NamedFunctionRange}s + * for improved debuggability and function naming. + * + * @throws + * If this resource was not produced by a sourcemap or if {@link ranges} are not nested properly. + * Concretely: For each range, start position must be less than end position, and + * there must be no "straddling" (i.e. partially overlapping ranges). + */ + setFunctionRangesForScript(ranges: NamedFunctionRange[]): Promise; + attachSourceMapURL(sourceMapURL: string): Promise; + } + + export interface InspectedWindow { + tabId: number; + + onResourceAdded: EventSink<(resource: Resource) => unknown>; + onResourceContentCommitted: EventSink<(resource: Resource, content: string) => unknown>; + + eval( + expression: string, + options?: {scriptExecutionContext?: string, frameURL?: string, useContentScriptContext?: boolean}, + callback?: (result: unknown, exceptioninfo: { + code: string, + description: string, + details: unknown[], + isError: boolean, + isException: boolean, + value: string + }) => unknown): void; + getResources(callback: (resources: Resource[]) => unknown): void; + reload(reloadOptions?: {ignoreCache?: boolean, injectedScript?: string, userAgent?: string}): void; + } + + export interface Button { + onClicked: EventSink<() => unknown>; + update(iconPath?: string, tooltipText?: string, disabled?: boolean): void; + } + + export interface ExtensionView { + onHidden: EventSink<() => unknown>; + onShown: EventSink<(window?: Window) => unknown>; + } + + export interface ExtensionPanel extends ExtensionView { + show(): void; + onSearch: EventSink<(action: string, queryString?: string) => unknown>; + createStatusBarButton(iconPath: string, tooltipText: string, disabled: boolean): Button; + } + + export interface RecorderView extends ExtensionView { + show(): void; + } + + export interface ExtensionSidebarPane extends ExtensionView { + setHeight(height: string): void; + setObject(jsonObject: string, rootTitle?: string, callback?: () => unknown): void; + setPage(path: string): void; + } + + export interface PanelWithSidebar { + createSidebarPane(title: string, callback?: (result: ExtensionSidebarPane) => unknown): void; + onSelectionChanged: EventSink<() => unknown>; + } + + export interface Panels { + elements: PanelWithSidebar; + sources: PanelWithSidebar; + network: NetworkPanel; + themeName: string; + + create(title: string, iconPath: string, pagePath: string, callback?: (panel: ExtensionPanel) => unknown): void; + openResource(url: string, lineNumber: number, columnNumber?: number, callback?: () => unknown): void; + + /** + * Fired when the theme changes in DevTools. + * + * @param callback The handler callback to register and be invoked on theme changes. + */ + setThemeChangeHandler(callback?: (themeName: string) => unknown): void; + } + + export interface Request { + getContent(callback: (content: string, encoding: string) => unknown): void; + } + + export interface Network { + onNavigated: EventSink<(url: string) => unknown>; + onRequestFinished: EventSink<(request: Request) => unknown>; + + getHAR(callback: (harLog: object) => unknown): void; + } + + export interface NetworkPanel { + show(options?: {filter: string}): Promise; + } + + export interface DevToolsAPI { + network: Network; + panels: Panels; + inspectedWindow: InspectedWindow; + languageServices: LanguageExtensions; + recorder: RecorderExtensions; + performance: Performance; + } + + export interface ExperimentalDevToolsAPI { + inspectedWindow: InspectedWindow; + } + + export interface RawModule { + url: string; + code?: ArrayBuffer; + } + + export interface RawLocationRange { + rawModuleId: string; + startOffset: number; + endOffset: number; + } + + export interface RawLocation { + rawModuleId: string; + codeOffset: number; + inlineFrameIndex: number; + } + + export interface SourceLocation { + rawModuleId: string; + sourceFileURL: string; + lineNumber: number; + columnNumber: number; + } + + export interface Variable { + scope: string; + name: string; + type: string; + nestedName?: string[]; + } + + export interface ScopeInfo { + type: string; + typeName: string; + icon?: string; + } + + export interface FunctionInfo { + name: string; + } + + export type RecorderExtensionPlugin = RecorderExtensionExportPlugin|RecorderExtensionReplayPlugin; + + export interface RecorderExtensionExportPlugin { + stringify(recording: Record): Promise; + stringifyStep(step: Record): Promise; + } + export interface RecorderExtensionReplayPlugin { + replay(recording: Record): void; + } + + export type RemoteObjectId = string; + export type RemoteObjectType = 'object'|'undefined'|'string'|'number'|'boolean'|'bigint'|'array'|'null'; + + export interface RemoteObject { + type: RemoteObjectType; + className?: string; + value?: any; + description?: string; + objectId?: RemoteObjectId; + linearMemoryAddress?: number; + linearMemorySize?: number; + hasChildren: boolean; + } + + /** + * This refers to a Javascript or a Wasm value of reference type + * in the V8 engine. We call it foreign object here to emphasize + * the difference with the remote objects managed by a language + * extension plugin. + */ + export interface ForeignObject { + type: 'reftype'; + valueClass: 'local'|'global'|'operand'; + index: number; + } + + export interface PropertyDescriptor { + name: string; + value: RemoteObject|ForeignObject; + } + + export interface LanguageExtensionPlugin { + /** + * A new raw module has been loaded. If the raw wasm module references an external debug info module, its URL will be + * passed as symbolsURL. + */ + addRawModule(rawModuleId: string, symbolsURL: string|undefined, rawModule: RawModule): + Promise; + + /** + * Find locations in raw modules from a location in a source file. + */ + sourceLocationToRawLocation(sourceLocation: SourceLocation): Promise; + + /** + * Find locations in source files from a location in a raw module. + */ + rawLocationToSourceLocation(rawLocation: RawLocation): Promise; + + /** + * Return detailed information about a scope. + */ + getScopeInfo(type: string): Promise; + + /** + * List all variables in lexical scope at a given location in a raw module. + */ + listVariablesInScope(rawLocation: RawLocation): Promise; + + /** + * Notifies the plugin that a script is removed. + */ + removeRawModule(rawModuleId: string): Promise; + + /** + * Retrieve function name(s) for the function(s) containing the rawLocation. This returns more than one entry if + * the location is inside of an inlined function with the innermost function at index 0. + */ + getFunctionInfo(rawLocation: RawLocation): + Promise<{frames: Array, missingSymbolFiles: Array}|{missingSymbolFiles: Array}| + {frames: Array}>; + + /** + * Find locations in raw modules corresponding to the inline function + * that rawLocation is in. Used for stepping out of an inline function. + */ + getInlinedFunctionRanges(rawLocation: RawLocation): Promise; + + /** + * Find locations in raw modules corresponding to inline functions + * called by the function or inline frame that rawLocation is in. + * Used for stepping over inline functions. + */ + getInlinedCalleesRanges(rawLocation: RawLocation): Promise; + + /** + * Retrieve a list of line numbers in a file for which line-to-raw-location mappings exist. + */ + getMappedLines(rawModuleId: string, sourceFileURL: string): Promise; + + /** + * Evaluate a source language expression in the context of a given raw location and a given stopId. stopId is an + * opaque key that should be passed to the APIs accessing wasm state, e.g., getWasmLinearMemory. A stopId is + * invalidated once the debugger resumes. + */ + evaluate(expression: string, context: RawLocation, stopId: unknown): Promise; + + /** + * Retrieve properties of the remote object identified by the object id. + */ + getProperties(objectId: RemoteObjectId): Promise; + + /** + * Permanently release the remote object identified by the object id. + */ + releaseObject(objectId: RemoteObjectId): Promise; + } + + + export interface SupportedScriptTypes { + language: string; + symbol_types: string[]; + } + + export type WasmValue = {type: 'i32'|'f32'|'f64', value: number}|{type: 'i64', value: bigint}| + {type: 'v128', value: string}|ForeignObject; + + export interface LanguageExtensions { + registerLanguageExtensionPlugin( + plugin: LanguageExtensionPlugin, pluginName: string, + supportedScriptTypes: SupportedScriptTypes): Promise; + unregisterLanguageExtensionPlugin(plugin: LanguageExtensionPlugin): Promise; + + getWasmLinearMemory(offset: number, length: number, stopId: unknown): Promise; + getWasmLocal(local: number, stopId: unknown): Promise; + getWasmGlobal(global: number, stopId: unknown): Promise; + getWasmOp(op: number, stopId: unknown): Promise; + + reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): + Promise; + } + + export interface Position { + line: number; + column: number; + } + + export interface NamedFunctionRange { + readonly name: string; + readonly start: Position; + readonly end: Position; + } + + export interface RecorderExtensions { + registerRecorderExtensionPlugin(plugin: RecorderExtensionPlugin, pluginName: string, mediaType?: string): + Promise; + unregisterRecorderExtensionPlugin(plugin: RecorderExtensionPlugin): Promise; + createView(title: string, pagePath: string): Promise; + } + + export interface Performance { + onProfilingStarted: EventSink<() => unknown>; + onProfilingStopped: EventSink<() => unknown>; + } + + export interface Chrome { + devtools: DevToolsAPI; + experimental: {devtools: ExperimentalDevToolsAPI}; + } + } +} + +declare global { + interface Window { + chrome: Chrome.DevTools.Chrome; + } +} diff --git a/extensions/cxx_debugging/CMakeLists.txt b/extensions/cxx_debugging/CMakeLists.txt new file mode 100644 index 0000000..3713d12 --- /dev/null +++ b/extensions/cxx_debugging/CMakeLists.txt @@ -0,0 +1,168 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +set(CXX_DEBUGGING_BUILD_REVISION 0 CACHE STRING "Project build revision") +set(CXX_DEBUGGING_BUILD_PATCH 0 CACHE STRING "Project build patch") + +cmake_minimum_required(VERSION 3.16) +cmake_policy(SET CMP0048 NEW) +project(DevToolsCXXDebuggingExtension VERSION 0.2.${CXX_DEBUGGING_BUILD_REVISION}.${CXX_DEBUGGING_BUILD_PATCH} LANGUAGES CXX) + +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD 17) +set(REPO_SOURCE_DIR ${PROJECT_SOURCE_DIR}/../..) +set(THIRD_PARTY_DIR ${PROJECT_SOURCE_DIR}/third_party) +set(DEVTOOLS_SOURCE_DIR ${REPO_SOURCE_DIR}) +set(CXX_DEBUGGING_GEN_DIR ${PROJECT_BINARY_DIR}/gen) +set(CXX_DEBUGGING_BINARY_DIR ${PROJECT_BINARY_DIR}/bin) +set(CXX_DEBUGGING_SOURCE_DIR ${PROJECT_SOURCE_DIR}) + +option(CXX_DEBUGGING_USE_SANITIZERS "Enable sanitizers" OFF) + +# Compile typescript sources +find_program(TS_COMPILER tsc PATHS ${DEVTOOLS_SOURCE_DIR}/node_modules/.bin REQUIRED NO_DEFAULT_PATH) + +set(TS_COMPILER_ARGS -p ${CMAKE_CURRENT_SOURCE_DIR} --outDir ${PROJECT_BINARY_DIR}) +exec_program(${TS_COMPILER} ${CMAKE_CURRENT_SOURCE_DIR} + ARGS ${TS_COMPILER_ARGS} --listFiles + OUTPUT_VARIABLE TS_COMPILER_INPUTS + RETURN_VALUE TS_COMPILER_RETVAL) + +if (NOT ${TS_COMPILER_RETVAL} EQUAL 0) + message(FATAL_ERROR "Running tsc failed:\n${TS_COMPILER_INPUTS}") +endif() + +string(REPLACE "\n" ";" TS_COMPILER_INPUTS ${TS_COMPILER_INPUTS}) + +set(TS_COMPILER_OUTPUTS) +foreach(tsc_input IN LISTS TS_COMPILER_INPUTS) + get_filename_component(ext ${tsc_input} EXT) + if (NOT ext MATCHES ".d.ts$") + file(RELATIVE_PATH rel_path ${CMAKE_CURRENT_SOURCE_DIR} ${tsc_input}) + get_filename_component(basename ${rel_path} NAME_WE) + get_filename_component(dirname ${rel_path} DIRECTORY) + list(APPEND TS_COMPILER_OUTPUTS ${CMAKE_CURRENT_BINARY_DIR}/${dirname}/${basename}.js) + endif() +endforeach() + +add_custom_command(OUTPUT ${TS_COMPILER_OUTPUTS} + COMMAND ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py --output + ${TS_COMPILER} ${TS_COMPILER_ARGS} + COMMENT "Compiling Typescript" + DEPENDS ${TS_COMPILER_INPUTS} tsconfig.json) + +add_custom_target(TypescriptOutput DEPENDS ${TS_COMPILER_OUTPUTS}) + +option(CXX_DEBUGGING_ENABLE_DWARF5 "Enable -gdwarf-5 for emitting DWARF5" OFF) +if(CXX_DEBUGGING_ENABLE_DWARF5) + add_compile_options(-gdwarf-5) + add_link_options(-gdwarf-5) +endif() + +option(CXX_DEBUGGING_ENABLE_PUBNAMES "Enable -gpubnames for emitting a DWARF index" OFF) +if(CXX_DEBUGGING_ENABLE_PUBNAMES) + add_compile_options(-gpubnames) + add_link_options(-gpubnames) +endif() + +# Build LLVM dependencies. +set(LLVM_TARGETS_TO_BUILD "WebAssembly" CACHE STRING "") +set(LLVM_ENABLE_PROJECTS "clang;lldb" CACHE STRING "") +set(LLDB_INCLUDE_TESTS "OFF" CACHE STRING "") +set(CLANG_INCLUDE_TESTS "OFF" CACHE STRING "") + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(CXX_DEBUGGING_BUILD_WASM TRUE) +else() + set(CXX_DEBUGGING_BUILD_WASM FALSE) +endif() + +if (CXX_DEBUGGING_BUILD_WASM) + set(LLVM_ENABLE_RTTI "ON" CACHE STRING "") + set(LLVM_ENABLE_THREADS "OFF" CACHE STRING "") +else() + set(LLVM_ON_WIN32 0) + set(LLVM_ON_UNIX 1) +endif() + + +if (CXX_DEBUGGING_BUILD_WASM) + link_libraries(-sWASM_BIGINT) + if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") + link_libraries( + -sREVERSE_DEPS=all + ) + endif() +endif() + +if (CXX_DEBUGGING_USE_SANITIZERS) + set(LLVM_USE_SANITIZER "Address;Undefined" CACHE STRING "") +else() + set(LLVM_USE_SANITIZER "") +endif() + +add_subdirectory(${THIRD_PARTY_DIR}/llvm/src/llvm ${CMAKE_CURRENT_BINARY_DIR}/third_party/llvm/src/llvm) +set_property(DIRECTORY ${THIRD_PARTY_DIR}/llvm/src/llvm PROPERTY EXCLUDE_FROM_ALL TRUE) + + +if (CXX_DEBUGGING_BUILD_WASM) + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(clangCodeGen PRIVATE -O2) + target_compile_options(obj.clangCodeGen PRIVATE -O2) + endif() +endif() + +# Required to enable llvm option parsing +include(${THIRD_PARTY_DIR}/llvm/src/llvm/cmake/modules/DetermineGCCCompatible.cmake) + +macro(copy_file INPUT OUTPUT) + get_filename_component(ABS_INPUT ${INPUT} ABSOLUTE BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + get_filename_component(ABS_OUTPUT ${OUTPUT} ABSOLUTE BASE_DIR ${CMAKE_CURRENT_BINARY_DIR}) + add_custom_command(OUTPUT ${OUTPUT} + DEPENDS ${INPUT} + COMMAND ${CMAKE_COMMAND} -E copy ${ABS_INPUT} ${ABS_OUTPUT} + ) +endmacro() + +function(fix_sysroot TARGET) + if (CMAKE_SYSROOT) + target_link_libraries(${TARGET} PRIVATE + -Wl,-rpath,${CMAKE_SYSROOT}/usr/lib/x86_64-linux-gnu + -Wl,-rpath,${CMAKE_SYSROOT}/lib/x86_64-linux-gnu + -Wl,-dynamic-linker,${CMAKE_SYSROOT}/lib64/ld-linux-x86-64.so.2 + ) + endif() +endfunction() + +set(LLVM_RUNTIME_OUTPUT_INTDIR ${LLVM_BINARY_DIR}/${CMAKE_CFG_INTDIR}/bin) +set(LLVM_LIBRARY_OUTPUT_INTDIR ${LLVM_BINARY_DIR}/${CMAKE_CFG_INTDIR}/lib${LLVM_LIBDIR_SUFFIX}) +set(LLVM_TOOLS_BINARY_DIR ${LLVM_RUNTIME_OUTPUT_INTDIR}) + +set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +set(CMAKE_SKIP_BUILD_RPATH FALSE) + +# Turn on -gsplit-dwarf if requested in debug builds. +option(CXX_DEBUGGING_USE_SPLIT_DWARF "Enable -gsplit-dwarf for the extension" OFF) +if(CXX_DEBUGGING_USE_SPLIT_DWARF) + if(NOT LLVM_USE_SPLIT_DWARF) + message(WARNING "CXX_DEBUGGING_USE_SPLIT_DWARF turned ON, but LLVM_USE_SPLIT_DWARF turned off, might not be what you want?") + endif() + string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) + if((uppercase_CMAKE_BUILD_TYPE STREQUAL "DEBUG") OR + (uppercase_CMAKE_BUILD_TYPE STREQUAL "RELWITHDEBINFO")) + add_compile_options(-gsplit-dwarf) + endif() +endif() + +fix_sysroot(lldb-tblgen) +fix_sysroot(clang-tblgen) +fix_sysroot(llvm-tblgen) +fix_sysroot(llvm-mc) +fix_sysroot(llvm-dwp) + +if (CXX_DEBUGGING_BUILD_WASM) + add_subdirectory(lib) + add_subdirectory(src) + add_subdirectory(tests) +endif() diff --git a/extensions/cxx_debugging/e2e/resources/huge-source-file.cc b/extensions/cxx_debugging/e2e/resources/huge-source-file.cc new file mode 100644 index 0000000..ab1874e --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/huge-source-file.cc @@ -0,0 +1,29 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +// Build a large type to produce a non-trivial DWO file, so that we can hit the +// mmap path inside of llvm. +template +struct Recursive; + +template <> +struct Recursive<0> { + int value = 1 << 31; +}; + +template +struct Recursive : Recursive {}; + +// Hide the local with the huge type to help the scope view a little bit. +int indirect() { + Recursive<1 << 10> r; + return r.value; +} + +int main() { + auto value = indirect(); + std::cout << value << '\n'; +} diff --git a/extensions/cxx_debugging/e2e/resources/pointers.cc b/extensions/cxx_debugging/e2e/resources/pointers.cc new file mode 100644 index 0000000..c0021c2 --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/pointers.cc @@ -0,0 +1,40 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +struct B; +struct A { + int value; + B* b; +}; + +struct B { + int value; + A* a; +}; + +int main() { + A a1 = {1}; + A a2 = {2}; + A a3 = {3}; + B b1 = {4}; + B b2 = {5}; + B b3 = {6}; + a1.b = &b1; + a2.b = &b2; + a3.b = &b3; + b1.a = &a2; + b2.a = &a3; + b3.a = nullptr; + + std::cout << a1.b->a->b->a->b->a << ": " << a1.b->a->b->a->b->value << "\n"; + + A cycle_a = {'a'}; + B cycle_b = {'b'}; + cycle_a.b = &cycle_b; + cycle_b.a = &cycle_a; + std::cout << cycle_a.b->a->b->a->b << ": " << cycle_a.b->a->b->a->b->value + << "\n"; +} diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c b/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c new file mode 100644 index 0000000..d5d4beb --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c @@ -0,0 +1,30 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +void check_non_primitives() { + int i = 10; + int* p = &i; + + int a[5] = {1, 2, 3, 4, 5}; + + struct Birthday { + int day; + int month; + int year; + }; + struct Birthday dob; + dob.day = 23; + dob.month = 6; + dob.year = 1912; + + printf("%p %i %i", p, a[0], dob.year); + return; +} + +int main() { + check_non_primitives(); +} diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp b/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp new file mode 100644 index 0000000..ad2c00b --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp @@ -0,0 +1,49 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +class Rectangle { + public: + int height; + int width; +}; + +enum UnscopedEnum { kUnscopedA = 19, kUnscopedB = 21 }; + +enum class ScopedEnum { kScopedA = 22, kScopedB = 23 }; + +struct Birthday { + int day; + int month; + int year; +}; + +void check_non_primitives() { + int a[5] = {1, 2, 3, 4, 5}; + + int i = 10; + int* p = &i; + int& r = i; + + UnscopedEnum u = kUnscopedB; + ScopedEnum s = ScopedEnum::kScopedB; + + Rectangle rec; + rec.height = 6; + rec.width = static_cast(s) + u; + + struct Birthday dob; + dob.day = 23; + dob.month = 6; + dob.year = 1912; + + printf("%d %p %d %d %d", a[0], p, r, rec.height, dob.year); + return; +} + +int main() { + check_non_primitives(); +} diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-primitives.c b/extensions/cxx_debugging/e2e/resources/scope-view-primitives.c new file mode 100644 index 0000000..37cb21a --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/scope-view-primitives.c @@ -0,0 +1,19 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +void check_primitives() { + int i = 10; + char c = 'a'; + float f = 1.1; + double d = 1.2; + printf("%d %c %f %f", i, c, f, d); + return; +} + +int main() { + check_primitives(); +} diff --git a/extensions/cxx_debugging/e2e/resources/stepping-with-state.c b/extensions/cxx_debugging/e2e/resources/stepping-with-state.c new file mode 100644 index 0000000..2db4975 --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/stepping-with-state.c @@ -0,0 +1,31 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +/* multiplies each element by a constant */ +void multiplyByConstant(int *array, int length, int constant) { + for (int i = 0; i < length; ++i) { + array[i] *= constant; + } +} + +int main() { + int n = 10; + int x[n]; + + /* initialize x */ + for (int i = 0; i < n; ++i) { + x[i] = i; + } + + /* multiply each element by 5 */ + multiplyByConstant(x, n, 5); + + /* output x */ + for (int i = 0; i < n; ++i) { + printf("x[%d] = %d\n", i, x[i]); + } + return 0; +} diff --git a/extensions/cxx_debugging/e2e/resources/test_wasm_simd.c b/extensions/cxx_debugging/e2e/resources/test_wasm_simd.c new file mode 100644 index 0000000..a50268a --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/test_wasm_simd.c @@ -0,0 +1,13 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +int main() { + v128_t a = wasm_i32x4_splat(1); + v128_t b = wasm_i32x4_splat(2); + v128_t c = wasm_i32x4_add(a, b); + int32_t r = wasm_i32x4_extract_lane(c, 0); + return r - 3; +} diff --git a/extensions/cxx_debugging/e2e/resources/vector.cc b/extensions/cxx_debugging/e2e/resources/vector.cc new file mode 100644 index 0000000..e1c1430 --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/vector.cc @@ -0,0 +1,14 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +int main() { + std::vector v(10); + v[4] = "foo"; + v.back() = "bar"; + + std::cerr << &v << " (" << &v[4] << "): " << v[4] << '\n'; + std::cerr << "bye" << v.size() << "\n"; +} diff --git a/extensions/cxx_debugging/e2e/resources/wchar.cc b/extensions/cxx_debugging/e2e/resources/wchar.cc new file mode 100644 index 0000000..2d9a17a --- /dev/null +++ b/extensions/cxx_debugging/e2e/resources/wchar.cc @@ -0,0 +1,22 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +int main() { + const wchar_t* c_str = L"abcde"; + std::wstring cxx_str = c_str; + std::wstring short_cxx_str = L"a"; + + const char16_t* u16_c_str = u"abcde"; + std::u16string u16_cxx_str = u"abcde"; + std::u16string u16_short_cxx_str = u"a"; + + const char32_t* u32_c_str = U"abcde"; + std::u32string u32_cxx_str = U"abcde"; + std::u32string u32_short_cxx_str = U"a"; + + std::wcout << c_str << '\n' << cxx_str << '\n' << short_cxx_str << '\n'; +} diff --git a/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml b/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml new file mode 100644 index 0000000..f95a999 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml @@ -0,0 +1,37 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Cpp expression evaluation +source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +use_dwo: +script: + - reason: setup + actions: + - file: stepping-with-state.c + action: set_breakpoint + breakpoint: 16 + - reason: breakpoint + file: stepping-with-state.c + line: 16 + evaluations: + - expression: n * 3 + value: 30 + - expression: '*&n' + value: 10 + - expression: 1 + 2 + value: 3 + - expression: 1 + 2*3 + value: 7 + - expression: 1 + (2 - 3) + value: 0 + - expression: 1 == 2 + value: false + - expression: 1 == 1 + value: true + - expression: (char)65 + value: "\"'A'\"" diff --git a/extensions/cxx_debugging/e2e/tests/pointer.yaml b/extensions/cxx_debugging/e2e/tests/pointer.yaml new file mode 100644 index 0000000..e060dd5 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/pointer.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Test Pointer Chains +source_file: //extensions/cxx_debugging/e2e/resources/pointers.cc +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: pointers.cc + action: set_breakpoint + breakpoint: 32 + - file: pointers.cc + action: set_breakpoint + breakpoint: 38 + + - reason: breakpoint + file: pointers.cc + line: 32 + variables: + - name: Local.a1.value + value: 1 + - name: Local.a1.b + type: B * + - name: Local.a1.b.$0 + type: B + - name: Local.a1.b.$0.value + value: 4 + - name: Local.a1.b.$0.a.$0.b.$0.a.$0.b.$0.a.$0 + value: null + + - reason: breakpoint + file: pointers.cc + line: 38 + variables: + - name: Local.cycle_a.b.$0.a.$0.b.$0.a.$0.b.$0.a.$0.b.$0.value + value: 98 diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml b/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml new file mode 100644 index 0000000..f7c1714 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml @@ -0,0 +1,35 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Scope view formats non primitive types correctly +# This test checks that the scope view displays +# arrays, structs and pointers correctly +source_file: //extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c +flags: [ + [-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: scope-view-non-primitives.c + action: set_breakpoint + breakpoint: 25 + - reason: breakpoint + file: scope-view-non-primitives.c + line: 25 + variables: + - name: Local.a + value: int[5] + - name: Local.a.0 + value: 1 + - name: Local.dob + value: Birthday + - name: Local.dob.year + value: 1912 + - name: Local.p + value: int * + - name: Local.p.$0 + value: 10 diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml b/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml new file mode 100644 index 0000000..58cbc95 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml @@ -0,0 +1,44 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Scope view formats non primitive types correctly for cpp +# This test checks that the scope view displays +# arrays, structs, references, pointers and +# classes correctly for C++ +source_file: //extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: scope-view-non-primitives.cpp + action: set_breakpoint + breakpoint: 43 + - reason: breakpoint + file: scope-view-non-primitives.cpp + line: 43 + variables: + - name: Local.a + value: int[5] + - name: Local.p + value: int * + - name: Local.p.$0 + value: 10 + - name: Local.r + value: 10 + - name: Local.dob + value: Birthday + - name: Local.dob.year + value: 1912 + - name: Local.rec + value: Rectangle + - name: Local.rec.height + value: 6 + - name: Local.s + value: kScopedB + - name: Local.u + value: kUnscopedB diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml b/extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml new file mode 100644 index 0000000..1211fb2 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Scope view formats primitive types correctly +# This test checks that the scope view displays +# integers, chars, float and doubles correctly +source_file: //extensions/cxx_debugging/e2e/resources/scope-view-primitives.c +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: scope-view-primitives.c + action: set_breakpoint + breakpoint: 14 + - reason: breakpoint + file: scope-view-primitives.c + line: 14 + variables: + - name: Local.i + value: 10 + - name: Local.c + value: "\"'a'\"" + - name: Local.f + value: 1.10000002 + - name: Local.d + value: 1.2 diff --git a/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml b/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml new file mode 100644 index 0000000..1a4d154 --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml @@ -0,0 +1,64 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Stepping with state +source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: stepping-with-state.c + action: set_breakpoint + breakpoint: 16 + - reason: breakpoint + file: stepping-with-state.c + line: 16 + variables: + - name: Local.n + value: 10 + - name: Local.x + value: 0 + actions: + - action: step_over + - reason: step + file: stepping-with-state.c + line: 19 + variables: + - name: Local.n + value: 10 + - name: Local.x + value: 0 + actions: + - action: remove_breakpoint + breakpoint: 16 + - file: stepping-with-state.c + action: set_breakpoint + breakpoint: 24 + - reason: breakpoint + file: stepping-with-state.c + line: 24 + variables: + - name: Local.n + value: 10 + - name: Local.x + value: 0 + actions: + - action: step_over + - reason: step + file: stepping-with-state.c + line: 27 + variables: + - name: Local.n + value: 10 + - name: Local.x + value: 0 + - name: Local.i + value: 0 + actions: + - action: remove_breakpoint + breakpoint: 24 diff --git a/extensions/cxx_debugging/e2e/tests/string_view.yaml b/extensions/cxx_debugging/e2e/tests/string_view.yaml new file mode 100644 index 0000000..f697b2c --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/string_view.yaml @@ -0,0 +1,32 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Test String View +source_file: //extensions/cxx_debugging/tests/inputs/string_view.cc +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: string_view.cc + action: set_breakpoint + breakpoint: 37 + - reason: breakpoint + file: string_view.cc + line: 37 + variables: + - name: Local.my_string + - name: Local.my_string.size + value: 12 + - name: Local.my_string.string + value: '"Hello World!"' + - name: Local.hello + type: 'MyStringView' + - name: Local.hello.end_ + value: 0 + - name: Local.hello.begin_ + value: 0 diff --git a/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml b/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml new file mode 100644 index 0000000..031917e --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml @@ -0,0 +1,21 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Successfully loads large dwo files +source_file: //extensions/cxx_debugging/e2e/resources/huge-source-file.cc +use_dwo: +flags: [ + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: huge-source-file.cc + action: set_breakpoint + breakpoint: 28 + - reason: breakpoint + file: huge-source-file.cc + line: 28 + variables: + - name: Local.value + value: -2147483648 diff --git a/extensions/cxx_debugging/e2e/tests/test_loop.yaml b/extensions/cxx_debugging/e2e/tests/test_loop.yaml new file mode 100644 index 0000000..01e6feb --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/test_loop.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: test_loop +source_file: //third_party/emscripten-releases/install/emscripten/tests/core/test_loop.c +flags: [[-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK]] +script: + - reason: setup + actions: + - file: test_loop.c + action: set_breakpoint + breakpoint: 11 + - file: test_loop.c + action: set_breakpoint + breakpoint: 13 + - reason: breakpoint + file: test_loop.c + line: 11 + variables: + - name: Local.x + value: '5' + - reason: breakpoint + file: test_loop.c + line: 13 + variables: + - name: Local.x + value: '5' + - reason: breakpoint + file: test_loop.c + line: 13 + variables: + - name: Local.x + value: '10' diff --git a/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml b/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml new file mode 100644 index 0000000..2b18eac --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: test_wasm_simd +source_file: //extensions/cxx_debugging/e2e/resources/test_wasm_simd.c +flags: [ + [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gdwarf-5], + [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gdwarf-5, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: test_wasm_simd.c + action: set_breakpoint + breakpoint: 10 + - file: test_wasm_simd.c + action: set_breakpoint + breakpoint: 12 + - reason: breakpoint + file: test_wasm_simd.c + line: 10 + variables: + - name: Local.a + value: 'v128_t' + - name: Local.a.0 + value: '1' + - name: Local.b + value: 'v128_t' + - name: Local.b.0 + value: '2' + - reason: breakpoint + file: test_wasm_simd.c + line: 12 + variables: + - name: Local.c + value: 'v128_t' + - name: Local.c.0 + value: '3' + - name: Local.r + value: '3' diff --git a/extensions/cxx_debugging/e2e/tests/vector.yaml b/extensions/cxx_debugging/e2e/tests/vector.yaml new file mode 100644 index 0000000..d7267fb --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/vector.yaml @@ -0,0 +1,26 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Test std::vector +source_file: //extensions/cxx_debugging/e2e/resources/vector.cc +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: vector.cc + action: set_breakpoint + breakpoint: 13 + - reason: breakpoint + file: vector.cc + line: 13 + variables: + - name: Local.v.4 + - name: Local.v.4.string + value: '"foo"' + - name: Local.v.9.string + value: '"bar"' diff --git a/extensions/cxx_debugging/e2e/tests/wchar.yaml b/extensions/cxx_debugging/e2e/tests/wchar.yaml new file mode 100644 index 0000000..1aa24af --- /dev/null +++ b/extensions/cxx_debugging/e2e/tests/wchar.yaml @@ -0,0 +1,52 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: Test Wide-Character Strings +source_file: //extensions/cxx_debugging/e2e/resources/wchar.cc +flags: [ + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] +script: + - reason: setup + actions: + - file: wchar.cc + action: set_breakpoint + breakpoint: 21 + - reason: breakpoint + file: wchar.cc + line: 21 + variables: + - name: Local.cxx_str + - name: Local.cxx_str.size + value: 5 + - name: Local.cxx_str.string + value: '"abcde"' + - name: Local.short_cxx_str.size + value: 1 + - name: Local.short_cxx_str.string + value: '"a"' + - name: Local.c_str + value: '"abcde"' + - name: Local.u16_cxx_str.size + value: 5 + - name: Local.u16_cxx_str.string + value: '"abcde"' + - name: Local.u16_short_cxx_str.size + value: 1 + - name: Local.u16_short_cxx_str.string + value: '"a"' + - name: Local.u16_c_str + value: '"abcde"' + - name: Local.u32_cxx_str.size + value: 5 + - name: Local.u32_cxx_str.string + value: '"abcde"' + - name: Local.u32_short_cxx_str.size + value: 1 + - name: Local.u32_short_cxx_str.string + value: '"a"' + - name: Local.u32_c_str + value: '"abcde"' diff --git a/extensions/cxx_debugging/lib/ApiContext.cc b/extensions/cxx_debugging/lib/ApiContext.cc new file mode 100644 index 0000000..23fdeac --- /dev/null +++ b/extensions/cxx_debugging/lib/ApiContext.cc @@ -0,0 +1,627 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ApiContext.h" +#include "Expressions.h" +#include "Variables.h" +#include "WasmModule.h" +#include "api.h" + +#include "lldb/Symbol/CompilerType.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-types.h" +#include "llvm/ADT/APFloat.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include "llvm/BinaryFormat/Wasm.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "symbols-backend" + +namespace symbols_backend { +namespace api { + +namespace { + +Error MakeError(Error::Code ec, llvm::Twine message) { + Error e; + e.SetCode(ec); + e.SetMessage(message.str()); + return e; +} + +Error MakeError(Error::Code ec, llvm::Error error) { + Error e; + e.SetCode(ec); + llvm::handleAllErrors(std::move(error), + [&e](const llvm::StringError& string_error) { + e.SetMessage(string_error.getMessage()); + }); + return e; +} + +Error MakeNotFoundError(llvm::StringRef raw_module_id) { + return MakeError(Error::Code::kModuleNotFoundError, + "Module with id '" + raw_module_id + "' not found"); +} + +Error MakeEvalError(llvm::Error error) { + Error e; + e.SetCode(Error::Code::kEvalError); + llvm::handleAllErrors(std::move(error), + [&e](const llvm::StringError& string_error) { + e.SetMessage(string_error.getMessage() + + " (Not all C++ expressions supported)"); + }); + return e; +} + +Variable::Scope ToApiScope(lldb::ValueType scope) { + switch (scope) { + case lldb::eValueTypeVariableGlobal: + case lldb::eValueTypeVariableStatic: + return Variable::Scope::kGlobal; + case lldb::eValueTypeVariableArgument: + return Variable::Scope::kParameter; + case lldb::eValueTypeVariableLocal: + return Variable::Scope::kLocal; + default: + llvm::errs() << "Got variable scope " << scope << "\n"; + llvm_unreachable("Unhandled variable scope"); + } +} + +llvm::SmallVector ToRawLocationRanges( + llvm::SmallSet, 1>&& ranges, + std::string raw_module_id) { + llvm::SmallVector locations; + for (auto offset_pair : ranges) { + RawLocationRange& location = locations.emplace_back(); + location.SetRawModuleId(raw_module_id); + location.SetStartOffset(offset_pair.first); + location.SetEndOffset(offset_pair.first + offset_pair.second); + } + return locations; +} +} // namespace + +// Notify the plugin about a new script +AddRawModuleResponse ApiContext::AddRawModule( + std::string raw_module_id, // A raw module identifier + std::string path // The path to the file with the wasm bytes +) { + AddRawModuleResponse response; + if (FindModule(raw_module_id)) { + response.SetError( + MakeError(Error::Code::kInternalError, + "Duplicate module with id '" + raw_module_id + "'")); + } else if (auto m = AddModule(raw_module_id, path)) { + auto source_info = m->GetSourceScripts(); + response.SetSources( + {source_info.sources.begin(), source_info.sources.end()}); + response.SetDwos({source_info.dwos.begin(), source_info.dwos.end()}); + } else { + response.SetError(MakeError(Error::Code::kInternalError, + "Unable to load file '" + path + "'")); + } + return response; +}; + +void ApiContext::RemoveRawModule( + std::string raw_module_id // A raw module identifier +) { + DeleteModule(raw_module_id); +} + +// Find locations in raw modules from a location in a source file +SourceLocationToRawLocationResponse ApiContext::SourceLocationToRawLocation( + std::string raw_module_id, + std::string source_file, + int32_t line_number, + int32_t column_number) { + SourceLocationToRawLocationResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + llvm::SmallVector locations = ToRawLocationRanges( + module->GetOffsetFromSourceLocation( + {source_file, static_cast(line_number + 1), + static_cast(column_number + 1)}), + raw_module_id); + response.SetRawLocationRanges({locations.begin(), locations.end()}); + return response; +}; + +// Find locations in source files from a location in a raw module +RawLocationToSourceLocationResponse ApiContext::RawLocationToSourceLocation( + std::string raw_module_id, + int32_t code_offset, + int32_t inline_frame_index) { + RawLocationToSourceLocationResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + llvm::SmallVector locations; + for (auto& source_loc : + module->GetSourceLocationFromOffset(code_offset, inline_frame_index)) { + SourceLocation& location = locations.emplace_back(); + location.SetSourceFile(source_loc.file); + location.SetRawModuleId(raw_module_id); + location.SetLineNumber(source_loc.line - 1); + location.SetColumnNumber(source_loc.column - 1); + } + response.SetSourceLocation({locations.begin(), locations.end()}); + return response; +} + +// List all variables in lexical scope at a location in a raw module +ListVariablesInScopeResponse ApiContext::ListVariablesInScope( + std::string raw_module_id, + int32_t code_offset, + int32_t inline_frame_index) { + ListVariablesInScopeResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + llvm::SmallVector variables; + for (auto& var : + module->GetVariablesInScope(code_offset, inline_frame_index)) { + if (var.name.empty()) { + // Skip anonymous objects, since we can't get their value through + // `evaluate`. + continue; + } + Variable& variable = variables.emplace_back(); + variable.SetScope(ToApiScope(var.scope)); + variable.SetName(var.name); + variable.SetType(var.type); + } + response.SetVariable({variables.begin(), variables.end()}); + return response; +} + +GetFunctionInfoResponse ApiContext::GetFunctionInfo(std::string raw_module_id, + int32_t code_offset) { + GetFunctionInfoResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + FunctionInfo fi = module->GetFunctionInfo(code_offset); + response.SetFunctionNames({fi.names.begin(), fi.names.end()}); + response.SetMissingSymbolFiles( + {fi.missing_symbols.begin(), fi.missing_symbols.end()}); + return response; +} + +namespace { +std::vector GetTypeNames(lldb_private::CompilerType type) { + std::vector names; + + lldb_private::CompilerType original_type = type; + + while (type.IsValid()) { + if (const char* type_name = type.GetDisplayTypeName().GetCString()) { + names.emplace_back(type_name); + } + if (const char* type_name = type.GetTypeName().GetCString()) { + if (names.back() != type_name) { + names.emplace_back(type_name); + } + } + type = type.GetTypedefedType(); + } + + // If the original input type is a fixed-size integer or enum type, append a + // matching c++ fixed-width integer type name to the list to simplify integer + // handling. + bool is_signed = false; + if (original_type.IsIntegerOrEnumerationType(is_signed)) { + if (original_type.IsEnumerationType(is_signed)) { + original_type = original_type.GetEnumerationIntegerType(); + } + if (auto byte_size = original_type.GetByteSize(nullptr)) { + switch (*byte_size) { + case 8: + names.push_back(is_signed ? "int64_t" : "uint64_t"); + break; + case 4: + names.push_back(is_signed ? "int32_t" : "uint32_t"); + break; + case 2: + names.push_back(is_signed ? "int16_t" : "uint16_t"); + break; + case 1: + names.push_back(is_signed ? "int8_t" : "uint8_t"); + break; + } + } + } + return names; +} + +} // namespace + +GetInlinedFunctionRangesResponse ApiContext::GetInlinedFunctionRanges( + std::string raw_module_id, + int32_t code_offset) { + GetInlinedFunctionRangesResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + llvm::SmallVector locations = ToRawLocationRanges( + module->GetInlineFunctionAddressRanges(code_offset), raw_module_id); + response.SetRawLocationRanges({locations.begin(), locations.end()}); + return response; +} + +GetInlinedCalleesRangesResponse ApiContext::GetInlinedCalleesRanges( + std::string raw_module_id, + int32_t code_offset) { + GetInlinedCalleesRangesResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + llvm::SmallVector locations = ToRawLocationRanges( + module->GetChildInlineFunctionAddressRanges(code_offset), raw_module_id); + response.SetRawLocationRanges({locations.begin(), locations.end()}); + return response; +} + +GetMappedLinesResponse ApiContext::GetMappedLines(std::string raw_module_id, + std::string source_file_url) { + GetMappedLinesResponse response; + auto module = FindModule(raw_module_id); + if (!module) { + response.SetError(MakeNotFoundError(raw_module_id)); + return response; + } + + std::vector lines = module->GetMappedLines(source_file_url); + for (int32_t& line : lines) { + --line; + } + response.SetMappedLines(std::move(lines)); + return response; +} + +std::shared_ptr ApiContext::AddModule(llvm::StringRef id, + llvm::StringRef path) { + if (!llvm::sys::fs::exists(path)) { + LLVM_DEBUG(llvm::dbgs() + << "Module '" << id << "' at " << path << " not found\n"); + return nullptr; + } + + auto maybe_module = WasmModule::CreateFromFile(path); + if (!maybe_module) { + llvm::errs() << llvm::toString(maybe_module.takeError()); + return nullptr; + } + const auto& module = + modules_.insert({id, std::move(*maybe_module)}).first->second; + if (module) { + LLVM_DEBUG(llvm::dbgs() << "Loaded module " << id << " with " + << module->GetSourceScripts().sources.size() + << " source files\n"); + } + return module; +} + +std::shared_ptr ApiContext::FindModule(llvm::StringRef id) const { + auto it = modules_.find(id); + if (it == modules_.end()) { + return nullptr; + } + return it->second; +} + +void ApiContext::DeleteModule(llvm::StringRef id) { + modules_.erase(id); +} + +api::TypeInfo ApiContext::GetApiTypeInfo( + lldb_private::CompilerType type, + const llvm::SmallVectorImpl& member_info) { + std::vector enumerators; + type.ForEachEnumerator([&](const lldb_private::CompilerType& type, + lldb_private::ConstString label, + const llvm::APSInt& value) { + enumerators.push_back(api::Enumerator() + .SetName(label.AsCString("")) + .SetValue(value.getExtValue()) + .SetTypeId(GetTypeId(type))); + return true; + }); + + auto size = type.GetByteSize(nullptr); + uint64_t array_size = 0; + size_t alignment = type.GetTypeBitAlign(nullptr).value_or(0) / 8; + bool has_elements = type.IsPointerOrReferenceType() || + type.IsArrayType(nullptr, &array_size, nullptr) || + type.IsVectorType(nullptr, &array_size); + bool can_expand = type.IsAggregateType() || has_elements; + bool is_signed = false; + bool has_value = + type.IsScalarType() || type.IsEnumerationType(is_signed) || has_elements; + std::vector members; + for (const auto& member : member_info) { + members.push_back(FieldInfo() + .SetName(member.MemberName().str()) + .SetOffset(member.OffsetInParent()) + .SetTypeId(GetTypeId(member.Type()))); + } + + return api::TypeInfo() + .SetCanExpand(can_expand) + .SetArraySize(array_size) + .SetHasValue(has_value) + .SetTypeId(GetTypeId(type)) + .SetTypeNames(GetTypeNames(type)) + .SetSize(size ? *size : 0) + .SetIsPointer(type.IsPointerOrReferenceType(nullptr)) + .SetAlignment(alignment) + .SetMembers(std::move(members)) + .SetEnumerators(std::move(enumerators)); +} + +llvm::Expected> ApiContext::GetApiTypeInfos( + lldb_private::CompilerType type, + int32_t required_type_depth) { + std::deque> queue{{type, 0}}; + std::vector type_infos; + llvm::DenseSet visited_types; + + while (!queue.empty()) { + lldb_private::CompilerType type; + int32_t depth; + std::tie(type, depth) = queue.front(); + queue.pop_front(); + if (required_type_depth > 0 && depth > required_type_depth) { + continue; + } + if (!visited_types.insert(type.GetOpaqueQualType()).second) { + continue; + } + + auto member_info = SubObjectInfo::GetMembers(type); + for (const auto& member : member_info) { + if (!visited_types.contains(member.Type().GetOpaqueQualType())) { + queue.push_back({member.Type(), depth + 1}); + } + } + type_infos.push_back(GetApiTypeInfo(type, member_info)); + } + return type_infos; +} + +std::string ApiContext::GetTypeId(lldb_private::CompilerType type) { + std::string key = + std::to_string(reinterpret_cast(type.GetOpaqueQualType())); + types_[key] = type; + return key; +} + +llvm::Optional ApiContext::GetTypeFromId( + llvm::StringRef type_id) { + auto it = types_.find(type_id.str()); + if (it == types_.end()) { + return llvm::None; + } + return it->getValue(); +} + +struct EvalVisitor { + ApiContext& context; + lldb_private::CompilerType type; + llvm::Optional address; + + EvalVisitor(ApiContext& context, + lldb_private::CompilerType type, + llvm::Optional address) + : context(context), type(type), address(address) {} + + api::EvaluateExpressionResponse MakeResponse() { + auto member_type_infos = context.GetApiTypeInfos(type, 0); + if (!member_type_infos) { + return api::EvaluateExpressionResponse().SetError( + MakeEvalError(member_type_infos.takeError())); + } + api::TypeInfo root_type = member_type_infos->front(); + return api::EvaluateExpressionResponse() + .SetTypeInfos(std::move(*member_type_infos)) + .SetRoot(root_type) + .SetMemoryAddress(address ? llvm::Optional(*address) + : llvm::None); + } + + // Most types are stringified in the JS wrapper, including most primitive + // types as implemented below. There's two exceptions that we're handling + // here because they're impossible/hard to do in JS, which is enum labels and + // floating point values. Stringifying floats and doubles in JS is subtly + // different than in C++ (or rather lldb), in particular for floats which + // have different level of precision than JS's number. + template + api::EvaluateExpressionResponse MakeResponse(T v) { + auto begin = reinterpret_cast(&v); + auto end = begin + sizeof(T); + std::vector data = {begin, end}; + + bool is_signed = false; + llvm::Optional enum_label; + if (type.IsEnumerationType(is_signed)) { + type.ForEachEnumerator([&enum_label, v](auto t, auto label, auto value) { + if (value == v) { + enum_label = std::string(label.GetStringRef()); + return false; + } + return true; + }); + } + + return MakeResponse().SetData(data).SetDisplayValue(std::move(enum_label)); + } + + auto operator()(std::monostate v) { return MakeResponse(); } + auto operator()(bool v) { return MakeResponse(v); } + auto operator()(int8_t v) { return MakeResponse(v); } + auto operator()(uint8_t v) { return MakeResponse(v); } + auto operator()(int16_t v) { return MakeResponse(v); } + auto operator()(uint16_t v) { return MakeResponse(v); } + auto operator()(int32_t v) { return MakeResponse(v); } + auto operator()(uint32_t v) { return MakeResponse(v); } + auto operator()(int64_t v) { return MakeResponse(v); } + auto operator()(uint64_t v) { return MakeResponse(v); } + auto operator()(float v) { + llvm::APFloat f(v); + llvm::SmallVector str; + f.toString(str, 0, 6); + return MakeResponse(v).SetDisplayValue(std::string(str.data(), str.size())); + } + auto operator()(double v) { + llvm::APFloat f(v); + llvm::SmallVector str; + f.toString(str, 0, 6); + return MakeResponse(v).SetDisplayValue(std::string(str.data(), str.size())); + } + auto operator()(void* v) { + return MakeResponse().SetLocation(reinterpret_cast(v)); + } + auto operator()(std::nullptr_t v) { return MakeResponse(0); } +}; + +llvm::Expected DebuggerProxy::ReadMemory(lldb::addr_t address, + void* buffer, + size_t size) const { + return proxy_.call("readMemory", static_cast(address), + reinterpret_cast(buffer), size); +} + +static llvm::Expected readWasmValue( + const emscripten::val& value) { + std::string type = value["type"].as(); + if (type == "i32") { + return DebuggerProxy::WasmValue{llvm::wasm::ValType::I32, + value["value"].as()}; + } + if (type == "i64") { + return DebuggerProxy::WasmValue{llvm::wasm::ValType::I64, + value["value"].as()}; + } + if (type == "f32") { + return DebuggerProxy::WasmValue{llvm::wasm::ValType::F32, + value["value"].as()}; + } + if (type == "f64") { + return DebuggerProxy::WasmValue{llvm::wasm::ValType::F64, + value["value"].as()}; + } + if (type == "reftype") { + // Only a scalar value can cross the DWARF interpreter, so we + // encode the value into a 64-bit integer. + int64_t encodedValue = value["index"].as(); + std::string valueClass = value["valueClass"].as(); + if (valueClass == "local") { + encodedValue |= (int64_t)1 << 32; + } else if (valueClass == "operand") { + encodedValue |= (int64_t)2 << 32; + } else if (valueClass != "global") { + llvm::errs() << "Got value class " << encodedValue << "\n"; + llvm_unreachable("Unhandled value class"); + } + return DebuggerProxy::WasmValue{llvm::wasm::ValType::I64, encodedValue}; + } + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Invalid value type %s", type.c_str()); +} + +llvm::Expected DebuggerProxy::GetGlobal( + size_t index) const { + return readWasmValue(proxy_.call("getGlobal", index)); +} +llvm::Expected DebuggerProxy::GetLocal( + size_t index) const { + return readWasmValue(proxy_.call("getLocal", index)); +} +llvm::Expected DebuggerProxy::GetOperand( + size_t index) const { + return readWasmValue(proxy_.call("getOperand", index)); +} + +api::EvaluateExpressionResponse ApiContext::EvaluateExpression( + RawLocation location, + std::string expression, + emscripten::val debug_proxy) { + std::string raw_module_id = location.GetRawModuleId(); + auto module = FindModule(raw_module_id); + if (!module) { + return api::EvaluateExpressionResponse().SetError( + MakeNotFoundError(raw_module_id)); + } + auto result = module->InterpretExpression( + location.GetCodeOffset(), location.GetInlineFrameIndex(), expression, + DebuggerProxy{debug_proxy}); + if (!result) { + return api::EvaluateExpressionResponse().SetError( + MakeError(Error::Code::kEvalError, result.takeError())); + } + + return std::visit(EvalVisitor(*this, result->type, result->address), + result->value); +} + +} // namespace api +} // namespace symbols_backend + +#ifndef NDEBUG +namespace { +struct Logging { + Logging() { + lldb_private::Log::Initialize(); + lldb_private::Log::ListAllLogChannels(llvm::errs()); + auto stream = std::make_shared(2, false, true); + lldb_private::Log::EnableLogChannel(stream, 0, "lldb", {"default"}, + llvm::errs()); + } +}; + +static Logging l; +} // namespace +#endif diff --git a/extensions/cxx_debugging/lib/ApiContext.h b/extensions/cxx_debugging/lib/ApiContext.h new file mode 100644 index 0000000..5664135 --- /dev/null +++ b/extensions/cxx_debugging/lib/ApiContext.h @@ -0,0 +1,119 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EXTENSIONS_CXX_DEBUGGING_API_CONTEXT_H_ +#define EXTENSIONS_CXX_DEBUGGING_API_CONTEXT_H_ + +#include "api.h" + +#include "emscripten/val.h" +#include "lldb/lldb-types.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/BinaryFormat/Wasm.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include +#include +#include + +namespace lldb_private { +class CompilerType; +} + +namespace symbols_backend { +class WasmModule; +class SubObjectInfo; +namespace api { + +class DebuggerProxy { + const emscripten::val& proxy_; + + public: + struct WasmValue { + llvm::wasm::ValType type; + std::variant value; + }; + + explicit DebuggerProxy(const emscripten::val& proxy) : proxy_(proxy) {} + llvm::Expected ReadMemory(lldb::addr_t address, + void* buffer, + size_t size) const; + llvm::Expected GetGlobal(size_t index) const; + llvm::Expected GetLocal(size_t index) const; + llvm::Expected GetOperand(size_t index) const; +}; + +class ApiContext : public DWARFSymbolsApi { + public: + AddRawModuleResponse AddRawModule(std::string raw_module_id, + std::string path) final; + + void RemoveRawModule(std::string raw_module_id) final; + + SourceLocationToRawLocationResponse SourceLocationToRawLocation( + std::string raw_module_id, + std::string source_file, + int32_t line_number, + int32_t column_number) final; + + RawLocationToSourceLocationResponse RawLocationToSourceLocation( + std::string raw_module_id, + int32_t code_offset, + int32_t inline_frame_index) final; + + ListVariablesInScopeResponse ListVariablesInScope( + std::string raw_module_id, + int32_t code_offset, + int32_t inline_frame_index) final; + + GetFunctionInfoResponse GetFunctionInfo(std::string raw_module_id, + int32_t code_offset) override; + + GetInlinedFunctionRangesResponse GetInlinedFunctionRanges( + std::string raw_module_id, + int32_t code_offset) override; + + GetInlinedCalleesRangesResponse GetInlinedCalleesRanges( + std::string raw_module_id, + int32_t code_offset) override; + + GetMappedLinesResponse GetMappedLines(std::string raw_module_id, + std::string source_file_url) override; + + EvaluateExpressionResponse EvaluateExpression( + RawLocation location, + std::string expression, + emscripten::val debug_proxy) final; + + private: + llvm::StringMap> modules_; + llvm::StringMap types_; + + std::shared_ptr AddModule(llvm::StringRef id, + llvm::StringRef path); + std::shared_ptr FindModule(llvm::StringRef id) const; + void DeleteModule(llvm::StringRef id); + + api::TypeInfo GetApiTypeInfo( + lldb_private::CompilerType type, + const llvm::SmallVectorImpl& member_info); + llvm::Expected> GetApiTypeInfos( + lldb_private::CompilerType type, + int32_t required_type_depth); + std::string GetTypeId(lldb_private::CompilerType type); + llvm::Optional GetTypeFromId( + llvm::StringRef type_id); + + friend struct EvalVisitor; +}; + +} // namespace api +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_API_CONTEXT_H_ diff --git a/extensions/cxx_debugging/lib/CMakeLists.txt b/extensions/cxx_debugging/lib/CMakeLists.txt new file mode 100644 index 0000000..5f17ca7 --- /dev/null +++ b/extensions/cxx_debugging/lib/CMakeLists.txt @@ -0,0 +1,53 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +llvm_add_library(DWARFSymbols + api.h + ApiContext.cc + ApiContext.h + Expressions.cc + Expressions.h + Variables.cc + Variables.h + WasmModule.cc + WasmModule.h + WasmVendorPlugins.h + WasmVendorPlugins.cc + + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/ast.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/ast.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/defines.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/parser.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/parser.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/parser_context.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/parser_context.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/type.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/type.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/eval.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/eval.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/value.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/value.h + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/context.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/context.h + LINK_LIBS liblldb + lldbHost + lldbInitialization + lldbPluginSymbolFileDWARF + lldbPluginObjectFileWasm + lldbPluginSymbolVendorWasm + ${LLDB_SYSTEM_LIBS} + LINK_COMPONENTS Support) + +if (NOT LLVM_REQUIRES_EH AND NOT LLVM_ENABLE_RTTI) + target_compile_options(DWARFSymbols PUBLIC -fno-rtti) +endif() + +get_target_property(LLDB_INCLUDE_DIRS lldbHost INCLUDE_DIRECTORIES) +target_include_directories(DWARFSymbols PUBLIC + ${PROJECT_SOURCE_DIR}/third_party/llvm/lldb/source + ${PROJECT_SOURCE_DIR}/third_party/llvm/lldb/include + ${LLDB_INCLUDE_DIRS} + ${PROJECT_BINARY_DIR}/third_party/llvm/lldb/include + ${THIRD_PARTY_DIR}/lldb-eval/src + ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/extensions/cxx_debugging/lib/Expressions.cc b/extensions/cxx_debugging/lib/Expressions.cc new file mode 100644 index 0000000..97edafe --- /dev/null +++ b/extensions/cxx_debugging/lib/Expressions.cc @@ -0,0 +1,303 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "Expressions.h" +#include "ApiContext.h" +#include "WasmModule.h" +#include "WasmVendorPlugins.h" + +#include "lldb-eval/ast.h" +#include "lldb-eval/context.h" +#include "lldb-eval/eval.h" +#include "lldb-eval/parser.h" +#include "lldb-eval/parser_context.h" +#include "lldb-eval/value.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/Section.h" +#include "lldb/Symbol/CompilerType.h" +#include "lldb/Symbol/SymbolContext.h" +#include "lldb/Symbol/Type.h" +#include "lldb/Symbol/TypeSystem.h" +#include "lldb/Target/Process.h" +#include "lldb/Utility/ArchSpec.h" +#include "lldb/Utility/Listener.h" +#include "llvm/ADT/StringRef.h" + +#include +#include +#include +#include +#include + +namespace symbols_backend { +static lldb_private::CompilerType GetTopType(lldb_private::CompilerType t) { + if (!t.IsValid() || !t.IsTypedefType()) { + return t; + } + return GetTopType(t.GetTypedefedType()); +} + +struct WasmValueLoaderContext : SymbolFileWasmDWARF::WasmValueLoader { + const api::DebuggerProxy& proxy; + + explicit WasmValueLoaderContext(const api::DebuggerProxy& proxy, + SymbolFileWasmDWARF& symbol_file) + : SymbolFileWasmDWARF::WasmValueLoader(symbol_file), proxy(proxy) {} + + llvm::Expected LoadWASMValue( + uint8_t storage_type, + const lldb_private::DataExtractor& data, + lldb::offset_t& offset) final { + uint64_t index = 0; + switch (storage_type) { + case 0x00: // local + case 0x01: // global + case 0x02: // operand + index = data.GetULEB128(&offset); + break; + case 0x03: // global i32 + index = data.GetU32(&offset); + break; + default: + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Invalid WASM value storage type"); + } + + switch (storage_type) { + case 0x00: // local + return proxy.GetLocal(index); + case 0x02: // operand + return proxy.GetOperand(index); + case 0x01: // global + case 0x03: // global i32 + return proxy.GetGlobal(index); + } + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Invalid WASM value storage type"); + } +}; + +lldb_private::CompilerType GetCompilerType(lldb::SBType t) { + struct Derived : lldb::SBType { + explicit Derived(const lldb::SBType& t) : SBType(t) {} + auto Get() { return ref().GetCompilerType(false); } + }; + return Derived{t}.Get(); +} + +llvm::Optional CheckError(lldb::SBValue value) { + auto e = value.GetError(); + if (!e.IsValid() || e.Success()) { + return {}; + } + auto message = e.GetCString(); + return llvm::createStringError(llvm::inconvertibleErrorCode(), message); +} + +llvm::Expected InterpretExpression( + const WasmModule& module, + lldb_private::TypeSystem& type_system, + lldb_private::SymbolContext& sc, + size_t frame_offset, + size_t inline_frame_index, + lldb_private::Address addr, + llvm::StringRef expression, + const api::DebuggerProxy& proxy) { + auto target = module.Target()->shared_from_this(); + lldb::ListenerSP listener = lldb_private::Listener::MakeListener("wasm32"); + + lldb::ProcessSP process = + target->CreateProcess(listener, "wasm32", nullptr, false); + if (!process) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Failed to create process"); + } + lldb::SectionSP code_section = + module.Module()->GetObjectFile()->GetSectionList()->FindSectionByType( + lldb::eSectionTypeCode, false); + target->SetSectionLoadAddress(code_section, 0); + + static_cast(process.get()) + ->SetProxyAndFrameOffset(proxy, frame_offset); + process->UpdateThreadListIfNeeded(); + process->GetThreadList().SetSelectedThreadByID(0); + auto thread = std::static_pointer_cast( + process->GetThreadList().GetSelectedThread()); + + WasmValueLoaderContext loader(proxy, *llvm::cast( + module.Module()->GetSymbolFile())); + + auto sm = lldb_eval::SourceManager::Create(expression.str()); + auto ctx = lldb_eval::Context::Create(sm, lldb::SBFrame{thread->GetFrame()}); + lldb_eval::Parser parser(ctx); + lldb_eval::Error e; + auto tree = parser.Run(e); + if (e || !tree || tree->is_error()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + e.message().c_str()); + } + lldb_eval::Interpreter interpreter(target, sm); + auto result = interpreter.Eval(tree.get(), e); + if (!result.IsValid() || e) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + e.message().c_str()); + } + + if (auto e = CheckError(result.inner_value())) { + return std::move(*e); + } + auto type = GetCompilerType(ToSBType(result.type())); + auto top_type = GetTopType(type); + auto val = result.inner_value(); + + llvm::Optional address; + if (auto address_of = val.AddressOf()) { + address = address_of.GetValueAsUnsigned(); + } + switch (top_type.GetBasicTypeEnumeration()) { + case lldb::BasicType::eBasicTypeChar: + case lldb::BasicType::eBasicTypeSignedChar: + case lldb::BasicType::eBasicTypeChar8: + case lldb::BasicType::eBasicTypeChar16: + case lldb::BasicType::eBasicTypeChar32: + case lldb::BasicType::eBasicTypeShort: + case lldb::BasicType::eBasicTypeInt: + case lldb::BasicType::eBasicTypeLong: + case lldb::BasicType::eBasicTypeWChar: + case lldb::BasicType::eBasicTypeSignedWChar: + case lldb::BasicType::eBasicTypeLongLong: { + auto integer = result.GetValueAsSigned(); + if (auto integer_size = type.GetByteSize(nullptr)) { + switch (*integer_size) { + case 1: + return ExpressionResult{type, static_cast(integer), + address}; + case 2: + return ExpressionResult{type, static_cast(integer), + address}; + case 4: + return ExpressionResult{type, static_cast(integer), + address}; + case 8: + return ExpressionResult{type, static_cast(integer), + address}; + } + break; + } + } + case lldb::BasicType::eBasicTypeUnsignedChar: + case lldb::BasicType::eBasicTypeUnsignedShort: + case lldb::BasicType::eBasicTypeUnsignedWChar: + case lldb::BasicType::eBasicTypeUnsignedInt: + case lldb::BasicType::eBasicTypeUnsignedLong: + case lldb::BasicType::eBasicTypeUnsignedLongLong: { + auto integer = result.GetUInt64(); + if (auto integer_size = type.GetByteSize(nullptr)) { + switch (*integer_size) { + case 1: + return ExpressionResult{type, static_cast(integer), + address}; + case 2: + return ExpressionResult{type, static_cast(integer), + address}; + case 4: + return ExpressionResult{type, static_cast(integer), + address}; + case 8: + return ExpressionResult{type, static_cast(integer), + address}; + } + } + break; + } + case lldb::BasicType::eBasicTypeBool: + return ExpressionResult{type, result.GetBool(), address}; + case lldb::BasicType::eBasicTypeFloat: + return ExpressionResult{type, result.GetFloat().convertToFloat(), + address}; + case lldb::BasicType::eBasicTypeDouble: + return ExpressionResult{type, result.GetFloat().convertToDouble(), + address}; + case lldb::BasicType::eBasicTypeVoid: + return ExpressionResult{type, std::monostate{}, address}; + case lldb::BasicType::eBasicTypeNullPtr: + return ExpressionResult{type, nullptr, address}; + case lldb::BasicType::eBasicTypeHalf: + case lldb::BasicType::eBasicTypeLongDouble: + case lldb::BasicType::eBasicTypeFloatComplex: + case lldb::BasicType::eBasicTypeDoubleComplex: + case lldb::BasicType::eBasicTypeLongDoubleComplex: + case lldb::BasicType::eBasicTypeObjCID: + case lldb::BasicType::eBasicTypeObjCClass: + case lldb::BasicType::eBasicTypeObjCSel: + case lldb::BasicType::eBasicTypeInt128: + case lldb::BasicType::eBasicTypeUnsignedInt128: + case lldb::BasicType::eBasicTypeOther: + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Invalid basic type %s", + result.type()->GetName().str().c_str()); + case lldb::BasicType::eBasicTypeInvalid: + break; + } + + bool is_signed = false; + if (top_type.IsEnumerationType(is_signed)) { + if (is_signed) { + auto integer = result.GetValueAsSigned(); + if (auto integer_size = type.GetByteSize(nullptr)) { + switch (*integer_size) { + case 1: + return ExpressionResult{type, static_cast(integer), + address}; + case 2: + return ExpressionResult{type, static_cast(integer), + address}; + case 4: + return ExpressionResult{type, static_cast(integer), + address}; + case 8: + return ExpressionResult{type, static_cast(integer), + address}; + } + } + } + auto integer = result.GetUInt64(); + if (auto integer_size = type.GetByteSize(nullptr)) { + switch (*integer_size) { + case 1: + return ExpressionResult{type, static_cast(integer), address}; + case 2: + return ExpressionResult{type, static_cast(integer), + address}; + case 4: + return ExpressionResult{type, static_cast(integer), + address}; + case 8: + return ExpressionResult{type, static_cast(integer), + address}; + } + } + } + + if (result.IsPointer()) { + if (module.Module()->GetArchitecture().GetAddressByteSize() == 4) { + return ExpressionResult{type, static_cast(result.GetUInt64()), + address}; + } else { + return ExpressionResult{type, static_cast(result.GetUInt64()), + address}; + } + } + + auto ptr = result.AddressOf(); + if (ptr.IsValid()) { + return ExpressionResult{type, reinterpret_cast(ptr.GetUInt64()), + address}; + } + + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Cannot evaluate in-memory value with type %s", + result.type()->GetName().str().c_str()); +} +} // namespace symbols_backend diff --git a/extensions/cxx_debugging/lib/Expressions.h b/extensions/cxx_debugging/lib/Expressions.h new file mode 100644 index 0000000..395d5d6 --- /dev/null +++ b/extensions/cxx_debugging/lib/Expressions.h @@ -0,0 +1,53 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EXTENSIONS_CXX_DEBUGGING_EXPRESSIONS_H_ +#define EXTENSIONS_CXX_DEBUGGING_EXPRESSIONS_H_ + +#include "lldb/Symbol/CompilerType.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" + +#include + +namespace symbols_backend { +namespace api { +class DebuggerProxy; +} +class WasmModule; + +struct ExpressionResult { + lldb_private::CompilerType type; + std::variant + value; + llvm::Optional address; +}; + +llvm::Expected InterpretExpression( + const WasmModule& module, + lldb_private::TypeSystem& type_system, + lldb_private::SymbolContext& sc, + size_t frame_offset, + size_t inline_frame_index, + lldb_private::Address addr, + llvm::StringRef expression, + const api::DebuggerProxy& proxy); + +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_EXPRESSIONS_H_ diff --git a/extensions/cxx_debugging/lib/Variables.cc b/extensions/cxx_debugging/lib/Variables.cc new file mode 100644 index 0000000..3da7ea3 --- /dev/null +++ b/extensions/cxx_debugging/lib/Variables.cc @@ -0,0 +1,101 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "Variables.h" + +#include "lldb/Utility/DataExtractor.h" +#include "llvm/ADT/Optional.h" +#include "llvm/BinaryFormat/Dwarf.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include + +namespace symbols_backend { +namespace { +template +llvm::Expected ChainErrors(llvm::Expected&& value, + llvm::Optional&& error) { + if (!error || value) { + if (error) { + llvm::consumeError(std::move(*error)); + } + return std::move(value); + } + return joinErrors(value.takeError(), std::move(*error)); +} +} // namespace + +static lldb_private::CompilerType BaseType(lldb_private::CompilerType t) { + while (t.IsTypedefType()) { + t = t.GetTypedefedType(); + } + return t; +} + +static void CreateMemberInfo(llvm::SmallVectorImpl& members, + lldb_private::CompilerType type) { + type = BaseType(type); + + for (size_t base_class = 0, e = type.GetNumDirectBaseClasses(); + base_class < e; ++base_class) { + CreateMemberInfo(members, + type.GetDirectBaseClassAtIndex(base_class, nullptr)); + } + + for (size_t base_class = 0, e = type.GetNumVirtualBaseClasses(); + base_class < e; ++base_class) { + CreateMemberInfo(members, + type.GetVirtualBaseClassAtIndex(base_class, nullptr)); + } + + for (size_t child = 0, e = type.GetNumFields(); child < e; ++child) { + std::string child_name; + uint64_t bit_offset; + auto child_type = + type.GetFieldAtIndex(child, child_name, &bit_offset, nullptr, nullptr); + assert(bit_offset % 8 == 0 && "Expecting fields to be byte-aligned"); + if (child_name.empty()) { + // TODO(pfaffe) Are unions truly the only case here? + child_name = ""; + } + members.emplace_back(child_name, bit_offset / 8, child_type); + } +} + +/*static */ llvm::SmallVector SubObjectInfo::GetMembers( + lldb_private::CompilerType type) { + type = BaseType(type); + + lldb_private::CompilerType pointee_type; + if (type.IsPointerType(&pointee_type)) { + SubObjectInfo info("*", 0, pointee_type); + return llvm::SmallVector{{info}}; + } + + if (type.IsArrayType(&pointee_type, nullptr, nullptr) || + type.IsVectorType(&pointee_type, nullptr)) { + SubObjectInfo info("0", 0, pointee_type); + return llvm::SmallVector{{info}}; + } + + if (type.IsReferenceType()) { + SubObjectInfo info("*", 0, type.GetNonReferenceType()); + return llvm::SmallVector{{info}}; + } + + if (type.IsAggregateType()) { + llvm::SmallVector members; + CreateMemberInfo(members, type); + return members; + } + return {}; +} + +} // namespace symbols_backend diff --git a/extensions/cxx_debugging/lib/Variables.h b/extensions/cxx_debugging/lib/Variables.h new file mode 100644 index 0000000..218582a --- /dev/null +++ b/extensions/cxx_debugging/lib/Variables.h @@ -0,0 +1,65 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EXTENSIONS_CXX_DEBUGGING_VARIABLES_H_ +#define EXTENSIONS_CXX_DEBUGGING_VARIABLES_H_ + +#include "lldb/Symbol/CompilerType.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include +#include + +namespace lldb_private { +class Function; +} // namespace lldb_private + +namespace symbols_backend { + +class ObjectInfoBase { + public: + bool IsSubObject() const { return is_subobject_; } + + bool IsArray() const { return type_.IsArrayType(nullptr, nullptr, nullptr); } + + bool IsPointer() const { return type_.IsPointerType(); } + bool IsReference() const { return type_.IsReferenceType(); } + + bool HasInner() const { + return IsArray() || IsPointer() || IsReference() || type_.IsAggregateType(); + } + + lldb_private::CompilerType Type() const { return type_; } + + protected: + ObjectInfoBase(bool is_subobject, lldb_private::CompilerType type) + : is_subobject_(is_subobject), type_(type) {} + bool is_subobject_; + lldb_private::CompilerType type_; +}; + +class SubObjectInfo : public ObjectInfoBase { + public: + static llvm::SmallVector GetMembers( + lldb_private::CompilerType type); + + SubObjectInfo(llvm::StringRef name, + uint64_t offset_in_parent, + lldb_private::CompilerType type) + : ObjectInfoBase(true, type), + identifier_(name), + offset_(offset_in_parent) {} + llvm::StringRef MemberName() const { return identifier_; } + + uint64_t OffsetInParent() const { return offset_; } + + protected: + std::string identifier_; + uint64_t offset_; +}; + +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_VARIABLES_H_ diff --git a/extensions/cxx_debugging/lib/WasmModule.cc b/extensions/cxx_debugging/lib/WasmModule.cc new file mode 100644 index 0000000..07c4b53 --- /dev/null +++ b/extensions/cxx_debugging/lib/WasmModule.cc @@ -0,0 +1,573 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "WasmModule.h" +#include "Expressions.h" +#include "WasmVendorPlugins.h" + +#include "Plugins/SymbolFile/DWARF/DWARFCompileUnit.h" +#include "Plugins/SymbolFile/DWARF/LogChannelDWARF.h" +#include "lldb/Core/Address.h" +#include "lldb/Core/AddressRange.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/FileSpecList.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/Section.h" +#include "lldb/Symbol/Block.h" +#include "lldb/Symbol/CompileUnit.h" +#include "lldb/Symbol/CompilerType.h" +#include "lldb/Symbol/Function.h" +#include "lldb/Symbol/LineEntry.h" +#include "lldb/Symbol/LineTable.h" +#include "lldb/Symbol/ObjectFile.h" +#include "lldb/Symbol/SymbolContext.h" +#include "lldb/Symbol/SymbolContextScope.h" +#include "lldb/Symbol/Type.h" +#include "lldb/Symbol/TypeList.h" +#include "lldb/Symbol/TypeSystem.h" +#include "lldb/Symbol/Variable.h" +#include "lldb/Symbol/VariableList.h" +#include "lldb/Utility/ArchSpec.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/FileSpec.h" +#include "lldb/Utility/Log.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "symbols-backend" + +namespace std { +template <> +struct less { + bool operator()(const symbols_backend::SourceLocation& a, + const symbols_backend::SourceLocation& b) const { + return std::make_tuple(a.file, a.line, a.column) < + std::make_tuple(b.file, b.line, b.column); + } +}; + +template <> +struct less { + bool operator()(const symbols_backend::Variable& a, + const symbols_backend::Variable& b) const { + return a.name < b.name; + } +}; +} // namespace std + +namespace { +static llvm::StringRef GetDWOName(DWARFCompileUnit& dwarf_cu) { + return dwarf_cu.GetUnitDIEOnly().GetDIE()->GetAttributeValueAsString( + &dwarf_cu, lldb_private::dwarf::DW_AT_dwo_name, nullptr); +} +} // namespace + +namespace symbols_backend { +bool operator==(const SourceLocation& a, const SourceLocation& b) { + return std::make_tuple(a.file, a.line, a.column) == + std::make_tuple(b.file, b.line, b.column); +} + +bool operator==(const symbols_backend::Variable& a, + const symbols_backend::Variable& b) { + return std::make_tuple(a.name, a.type, a.scope) == + std::make_tuple(b.name, b.type, b.scope); +} + +llvm::Expected> GetTarget() { + static std::pair instance = {}; + if (!instance.first) { + lldb::DebuggerSP dbg = lldb_private::Debugger::CreateInstance(); + lldb_private::ArchSpec arch("wasm32-unknown-unknown"); + lldb::TargetSP target; + lldb::PlatformSP platform = lldb_private::Platform::GetHostPlatform(); + dbg->GetPlatformList().SetSelectedPlatform(platform); + auto stat = dbg->GetTargetList().CreateTarget( + *dbg, {}, arch, lldb_private::eLoadDependentsNo, platform, target); + if (stat.Fail()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + stat.AsCString()); + } + target->SetPreloadSymbols(false); + instance = {dbg, target}; + } + return instance; +} + +llvm::Expected> WasmModule::CreateFromFile( + llvm::StringRef path) { + if (!llvm::sys::fs::exists(path)) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Module file '%s' not found\n.", + path.str().c_str()); + } + LLVM_DEBUG(llvm::dbgs() << "Loading module from '" << path << "'\n"); + + auto instance = GetTarget(); + if (!instance) { + return instance.takeError(); + } + auto [debugger, target] = *instance; + auto module = target->GetOrCreateModule( + {lldb_private::FileSpec(path), + lldb_private::ArchSpec("wasm32-unknown-unknown-wasm")}, + false); + + return std::unique_ptr(new WasmModule(target, module)); +} + +bool WasmModule::Valid() const { + return module_ && module_->GetNumCompileUnits() > 0; +} + +SourceInfo WasmModule::GetSourceScripts() const { + llvm::SmallSet compile_units; + llvm::SmallSet dwos; + llvm::SmallSet, 1> all_files; + for (size_t idx = 0; idx < module_->GetNumCompileUnits(); idx++) { + auto compile_unit = module_->GetCompileUnitAtIndex(idx); + for (auto f : compile_unit->GetSupportFiles()) { + auto dir = f.GetDirectory().GetStringRef(); + auto filename = f.GetFilename().GetStringRef(); + if (filename.empty()) { + continue; + } + if (!all_files.insert(std::make_pair(dir, filename)).second) { + continue; + } + compile_units.insert(f.GetPath()); + } + + // Cast user data to DwarfUnit + DWARFCompileUnit* dwarf_cu = + static_cast(compile_unit->GetUserData()); + if (dwarf_cu && dwarf_cu->GetVersion() >= 5) { + // Might need to lazy load this .dwo (only works for DWARF5) + llvm::SmallVector missing_symbols; + auto dwo_name = GetDWOName(*dwarf_cu); + if (!dwo_name.empty()) { + dwos.insert(dwo_name.str()); + } + } + } + return {compile_units, dwos}; +} + +namespace { +uint32_t GetSymbolContextFromOffset(lldb_private::Module* module, + lldb::addr_t offset, + int inline_frame_index, + lldb::SymbolContextItem resolve_scope, + lldb_private::SymbolContext& out_sc, + lldb_private::Address& out_addr) { + if (!module->GetObjectFile() || !module->GetObjectFile()->GetSectionList()) { + return 0; + } + lldb_private::SymbolContext sc; + sc.module_sp = module->shared_from_this(); + lldb::SectionSP section = + module->GetObjectFile()->GetSectionList()->FindSectionByType( + lldb::eSectionTypeCode, false); + lldb_private::Address addr(section, offset); + + auto resolved = + module->ResolveSymbolContextForAddress(addr, resolve_scope, sc); + if (inline_frame_index == 0 && + (resolved & lldb::eSymbolContextBlock) != lldb::eSymbolContextBlock) { + out_sc = std::move(sc); + out_addr = std::move(addr); + return out_sc.GetResolvedMask(); + } + + for (int i = 0; i < inline_frame_index; i++) { + lldb_private::SymbolContext next_sc; + lldb_private::Address next_addr; + if (!sc.GetParentOfInlinedScope(addr, next_sc, next_addr)) { + return 0; + } + addr = std::move(next_addr); + sc = std::move(next_sc); + // Sometimes the inline block address range isn't properly clipped + // to the parent range; fix this. + if (addr.GetOffset() == 0 && sc.function) { + addr = sc.function->GetAddressRange().GetBaseAddress(); + } + } + out_sc = std::move(sc); + out_addr = std::move(addr); + return out_sc.GetResolvedMask(); +} + +void GetVariablesFromOffset(lldb_private::Module* module, + lldb::addr_t offset, + int inline_frame_index, + lldb_private::VariableList* var_list) { + lldb_private::SymbolContext sc; + lldb_private::Address addr; + auto resolved = GetSymbolContextFromOffset( + module, offset, inline_frame_index, lldb::eSymbolContextBlock, sc, addr); + if ((resolved & lldb::eSymbolContextBlock) == lldb::eSymbolContextBlock) { + sc.block->AppendVariables( + true, true, true, + [var_list](lldb_private::Variable* var) { + return (var_list->FindVariableIndex(lldb::VariableSP{ + var, [](lldb_private::Variable*) {}}) == UINT32_MAX); + }, + var_list); + } + resolved = GetSymbolContextFromOffset(module, offset, inline_frame_index, + lldb::eSymbolContextCompUnit, sc, addr); + if ((resolved & lldb::eSymbolContextCompUnit) == + lldb::eSymbolContextCompUnit) { + sc.comp_unit->GetVariableList(true)->AppendVariablesIfUnique(*var_list); + } +} +} // namespace + +lldb::VariableSP WasmModule::FindVariableAtOffset(lldb::addr_t offset, + int inline_frame_index, + llvm::StringRef name) const { + lldb_private::VariableList var_list; + GetVariablesFromOffset(&*module_, offset, inline_frame_index, &var_list); + // GetVariablesFromOffset fills the list with variables sorted from innermost + // scope to outermost scope, so the first hit in the list is the correct one. + for (auto var : var_list) { + // llvm::errs() << "var: " << var.get() << "\n"; + if (var->GetName().GetStringRef() == name) { + return var; + } + } + return {}; +} + +llvm::Optional WasmModule::FindType( + llvm::StringRef name) const { + lldb_private::TypeList type_list; + llvm::DenseSet searched_symbol_files; + module_->FindTypes(lldb_private::ConstString(name), true, 1, + searched_symbol_files, type_list); + if (!type_list.Empty()) { + return type_list.GetTypeAtIndex(0)->GetFullCompilerType(); + } + return llvm::None; +} + +llvm::SmallSet WasmModule::GetSourceLocationFromOffset( + lldb::addr_t offset, + int inline_frame_index) const { + llvm::SmallSet lines; + + lldb_private::Address addr; + lldb_private::SymbolContext sc; + auto resolved = GetSymbolContextFromOffset( + &*module_, offset, inline_frame_index, + lldb::eSymbolContextBlock | lldb::eSymbolContextLineEntry, sc, addr); + if ((resolved & lldb::eSymbolContextLineEntry) && sc.line_entry.IsValid() && + sc.line_entry.line > 0) { + lines.insert({sc.line_entry.file.GetPath(), sc.line_entry.line, + sc.line_entry.column}); + } + return lines; +} + +std::vector WasmModule::GetMappedLines( + llvm::StringRef file_path) const { + lldb_private::FileSpec::Style file_path_style = + lldb_private::FileSpec::GuessPathStyle(file_path).value_or( + lldb_private::FileSpec::Style::native); + lldb_private::FileSpec file_spec(file_path, file_path_style); + lldb_private::SymbolContextList line_entry_scs; + + // Get the comp unit symbol contexts for the file. + for (size_t idx = 0; idx < module_->GetNumCompileUnits(); idx++) { + auto compile_unit = module_->GetCompileUnitAtIndex(idx); + uint32_t file_idx = + compile_unit->GetSupportFiles().FindFileIndex(0, file_spec, true); + + // Gather all line entries for the compile_unit + lldb_private::LineTable* table = compile_unit->GetLineTable(); + while (file_idx != UINT32_MAX) { + table->FineLineEntriesForFileIndex(file_idx, true, line_entry_scs); + file_idx = compile_unit->GetSupportFiles().FindFileIndex(file_idx + 1, + file_spec, true); + } + } + + std::vector line_numbers; + for (const lldb_private::SymbolContext& line_sc : + line_entry_scs.SymbolContexts()) { + assert(line_sc.line_entry.IsValid()); + line_numbers.push_back(line_sc.line_entry.line); + } + + std::sort(line_numbers.begin(), line_numbers.end()); + auto end = std::unique(line_numbers.begin(), line_numbers.end()); + line_numbers.resize(end - line_numbers.begin()); + + return line_numbers; +} + +llvm::SmallSet, 1> +WasmModule::GetOffsetFromSourceLocation( + const SourceLocation& source_loc) const { + llvm::SmallSet, 1> locations; + std::vector output_local, output_extern; + + llvm::StringRef file_path(source_loc.file); + lldb_private::FileSpec::Style file_path_style = + lldb_private::FileSpec::GuessPathStyle(file_path).value_or( + lldb_private::FileSpec::Style::native); + lldb_private::FileSpec file_spec(file_path, file_path_style); + lldb_private::SymbolContextList list; + module_->ResolveSymbolContextsForFileSpec( + file_spec, source_loc.line, true, + lldb::eSymbolContextLineEntry | lldb::eSymbolContextCompUnit, list); + std::vector ranges; + for (lldb_private::SymbolContext sc : list.SymbolContexts()) { + if (!sc.line_entry.IsValid()) { + continue; + } + + // Only return positions in the same line. + if (sc.line_entry.line != source_loc.line) { + continue; + } + + // Take into account the column number here to make in-line breakpoints + // work. + if (source_loc.column && sc.line_entry.column != source_loc.column) { + continue; + } + + // Check if the line entry range is already covered. + lldb_private::AddressRange range = sc.line_entry.range; + if (std::find_if(ranges.begin(), ranges.end(), + [&](const lldb_private::AddressRange& r) { + auto address = range.GetBaseAddress(); + return r.ContainsFileAddress(address); + }) != ranges.end()) { + continue; + } + + while (true) { + lldb_private::SymbolContext next_sc; + lldb_private::Address range_end(range.GetBaseAddress()); + range_end.Slide(range.GetByteSize()); + range_end.CalculateSymbolContext(&next_sc, lldb::eSymbolContextLineEntry); + + // Don't combine ranges across "start of statement" markers inserted + // by the compiler. + if (!next_sc.line_entry.IsValid() || + next_sc.line_entry.is_start_of_statement || + next_sc.line_entry.range.GetByteSize() == 0) { + break; + } + + // Include any line 0 entries, they indicate that this is compiler- + // generated code that does not correspond to user source code. + if (next_sc.line_entry.original_file != sc.line_entry.original_file || + (next_sc.line_entry.line != sc.line_entry.line && + next_sc.line_entry.line != 0)) { + break; + } + + // Try to extend our address range to cover this line entry. + if (!range.Extend(next_sc.line_entry.range)) { + break; + } + } + + ranges.push_back(range); + } + + for (auto const& range : ranges) { + locations.insert({range.GetBaseAddress().GetOffset(), range.GetByteSize()}); + } + return locations; +} + +std::set WasmModule::GetVariablesInScope( + lldb::addr_t offset, + int inline_frame_index) const { + std::set variables; + lldb_private::VariableList var_list; + GetVariablesFromOffset(&*module_, offset, inline_frame_index, &var_list); + LLVM_DEBUG(llvm::dbgs() << "Found " << var_list.GetSize() << " variables\n"); + for (auto var : var_list) { + var->GetSymbolContextScope()->CalculateSymbolContextCompileUnit(); + // var_list contains variables sorted from innermost scope to outermost + // scope. The set compares variables by name to preserve shadowing order. + auto type = var->GetType(); + variables.insert( + {var->GetName().GetStringRef(), var->GetScope(), + type ? type->GetQualifiedName().GetStringRef() : llvm::StringRef()}); + } + return variables; +} + +FunctionInfo WasmModule::GetFunctionInfo(lldb::addr_t offset) const { + llvm::SmallVector function_names; + + lldb_private::SymbolContext sc, old_sc; + lldb_private::Address addr, old_addr; + auto resolved = GetSymbolContextFromOffset( + &*module_, offset, 0, lldb::eSymbolContextBlock, sc, addr); + if ((resolved & lldb::eSymbolContextBlock) == lldb::eSymbolContextBlock) { + do { + auto name = sc.GetFunctionName(); + if (name.IsNull() || name.IsEmpty()) { + function_names.push_back(""); + } else { + function_names.push_back(name.GetCString()); + } + old_sc = std::move(sc); + old_addr = std::move(addr); + } while (old_sc.GetParentOfInlinedScope(old_addr, sc, addr)); + return {function_names, {}}; + } else if ((resolved & lldb::eSymbolContextCompUnit) == + lldb::eSymbolContextCompUnit) { + // Compile unit might be missing symbols? + + // Cast user data to DwarfUnit + DWARFCompileUnit* dwarf_cu = + static_cast(sc.comp_unit->GetUserData()); + if (dwarf_cu && dwarf_cu == &dwarf_cu->GetNonSkeletonUnit()) { + // The skeleton unit is the only unit, but is there supposed to be a .dwo? + llvm::SmallVector missing_symbols; + auto dwo_name = GetDWOName(*dwarf_cu); + if (!dwo_name.empty()) { + missing_symbols.push_back(dwo_name.str()); + } + return {{}, missing_symbols}; + } + } + return {{}, {}}; +} + +llvm::SmallSet, 1> +WasmModule::GetInlineFunctionAddressRanges(lldb::addr_t offset) const { + llvm::SmallSet, 1> ranges; + lldb_private::SymbolContext sc; + lldb_private::Address addr; + auto resolved = GetSymbolContextFromOffset( + &*module_, offset, 0, lldb::eSymbolContextBlock, sc, addr); + if ((resolved & lldb::eSymbolContextBlock) == lldb::eSymbolContextBlock) { + lldb_private::Block* inline_block = sc.block->GetContainingInlinedBlock(); + if (inline_block) { + size_t count = inline_block->GetNumRanges(); + for (size_t i = 0; i < count; i++) { + lldb_private::AddressRange range; + if (inline_block->GetRangeAtIndex(i, range)) { + ranges.insert( + {range.GetBaseAddress().GetOffset(), range.GetByteSize()}); + } + } + } + } + return ranges; +} + +llvm::SmallSet, 1> +WasmModule::GetChildInlineFunctionAddressRanges(lldb::addr_t offset) const { + llvm::SmallSet, 1> ranges; + lldb_private::SymbolContext sc; + lldb_private::Address addr; + auto resolved = GetSymbolContextFromOffset( + &*module_, offset, 0, lldb::eSymbolContextBlock, sc, addr); + if ((resolved & lldb::eSymbolContextBlock) == lldb::eSymbolContextBlock) { + // Find root block for function to search from + lldb_private::Block* root_block = sc.block; + while (!root_block->GetInlinedFunctionInfo()) { + lldb_private::Block* parent = root_block->GetParent(); + if (parent) { + root_block = parent; + } else { + break; + } + } + + // Traverse tree to find child inline blocks + lldb_private::Block* block = root_block->GetFirstChild(); + while (block) { + lldb_private::Block* next_block = nullptr; + if (block->GetInlinedFunctionInfo()) { + size_t count = block->GetNumRanges(); + for (size_t i = 0; i < count; i++) { + lldb_private::AddressRange range; + if (block->GetRangeAtIndex(i, range)) { + ranges.insert( + {range.GetBaseAddress().GetOffset(), range.GetByteSize()}); + } + } + } else { + // Only traverse children when not an inline block + next_block = block->GetFirstChild(); + } + // If we haven't found our next block, get a sibling + // or a parent's sibling + if (!next_block) { + while (true) { + next_block = block->GetSibling(); + if (next_block) { + break; + } + block = block->GetParent(); + if (!block || block == root_block) { + break; + } + } + } + block = next_block; + } + } + return ranges; +} + +llvm::Expected WasmModule::InterpretExpression( + lldb::addr_t frame_offset, + uint32_t inline_frame_index, + llvm::StringRef expression, + const api::DebuggerProxy& proxy) const { + lldb_private::SymbolContext sc; + lldb_private::Address addr; + auto resolved = + GetSymbolContextFromOffset(&*module_, frame_offset, inline_frame_index, + lldb::eSymbolContextEverything, sc, addr); + + if ((resolved & + (lldb::eSymbolContextCompUnit | lldb::eSymbolContextFunction)) == 0) { + resolved = + GetSymbolContextFromOffset(&*module_, frame_offset, inline_frame_index, + lldb::eSymbolContextCompUnit, sc, addr); + if ((resolved & lldb::eSymbolContextCompUnit) == 0) { + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "Not in a valid symbol context at offset %zu", frame_offset); + } + } + auto type_system = + module_->GetTypeSystemForLanguage(sc.comp_unit->GetLanguage()); + if (!type_system) { + return type_system.takeError(); + } + return ::symbols_backend::InterpretExpression( + *this, **type_system, sc, frame_offset, inline_frame_index, addr, + expression, proxy); +} +} // namespace symbols_backend diff --git a/extensions/cxx_debugging/lib/WasmModule.h b/extensions/cxx_debugging/lib/WasmModule.h new file mode 100644 index 0000000..2ea188c --- /dev/null +++ b/extensions/cxx_debugging/lib/WasmModule.h @@ -0,0 +1,126 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ +#define EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" +#include "lldb/lldb-types.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include + +namespace symbols_backend { +namespace api { +class DebuggerProxy; +} +class ObjectInfo; + +struct SourceLocation { + SourceLocation(llvm::StringRef file, uint32_t line, uint16_t column) + : file(file), line(line), column(column) {} + + std::string file; + uint32_t line; + uint16_t column; + + friend bool operator==(const SourceLocation&, const SourceLocation&); +}; + +struct Variable { + Variable(llvm::StringRef name, lldb::ValueType scope, llvm::StringRef type) + : name(name), scope(scope), type(type) {} + + std::string name; + lldb::ValueType scope; + std::string type; + + friend bool operator==(const Variable&, const Variable&); +}; + +struct FunctionInfo { + FunctionInfo(llvm::SmallVector names, + llvm::SmallVector missing_symbols) + : names(names), missing_symbols(missing_symbols) {} + + llvm::SmallVector names; + llvm::SmallVector missing_symbols; +}; + +struct SourceInfo { + SourceInfo(llvm::SmallSet sources, + llvm::SmallSet dwos) + : sources(sources), dwos(dwos) {} + + llvm::SmallSet sources; + llvm::SmallSet dwos; +}; + +struct ExpressionResult; +class WasmModule { + lldb::ModuleSP module_; + lldb::TargetSP target_; + explicit WasmModule(lldb::TargetSP target, lldb::ModuleSP module) + : module_(module), target_(target) {} + + public: + ~WasmModule(){}; + WasmModule(const WasmModule&) = delete; + WasmModule& operator=(const WasmModule&) = delete; + WasmModule(WasmModule&&) = default; + WasmModule& operator=(WasmModule&&) = default; + + static llvm::Expected> CreateFromFile( + llvm::StringRef path); + + bool Valid() const; + + lldb_private::Module* Module() const { return module_.get(); } + lldb_private::Target* Target() const { return target_.get(); } + + SourceInfo GetSourceScripts() const; + llvm::SmallSet GetSourceLocationFromOffset( + lldb::addr_t offset, + int inline_frame_index) const; + llvm::SmallSet, 1> + GetOffsetFromSourceLocation(const SourceLocation& source_loc) const; + std::set GetVariablesInScope(lldb::addr_t offset, + int inline_frame_index) const; + FunctionInfo GetFunctionInfo(lldb::addr_t offset) const; + // Returns inline function address ranges for the inline function containing + // offset, or an empty set if offset is not in an inline function. Used for + // stepping out of inline functions. + llvm::SmallSet, 1> + GetInlineFunctionAddressRanges(lldb::addr_t offset) const; + // Returns address ranges for any inline function calls made by the function + // or inline function containing offset. Used for stepping over inline + // functions. + llvm::SmallSet, 1> + GetChildInlineFunctionAddressRanges(lldb::addr_t offset) const; + + std::vector GetMappedLines(llvm::StringRef file) const; + + lldb::VariableSP FindVariableAtOffset(lldb::addr_t offset, + int inline_frame_index, + llvm::StringRef name) const; + llvm::Optional FindType( + llvm::StringRef name) const; + + llvm::Expected InterpretExpression( + lldb::addr_t frame_offset, + uint32_t inline_frame_index, + llvm::StringRef expression, + const api::DebuggerProxy& proxy) const; +}; + +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ diff --git a/extensions/cxx_debugging/lib/WasmVendorPlugins.cc b/extensions/cxx_debugging/lib/WasmVendorPlugins.cc new file mode 100644 index 0000000..7af53b5 --- /dev/null +++ b/extensions/cxx_debugging/lib/WasmVendorPlugins.cc @@ -0,0 +1,172 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "WasmVendorPlugins.h" + +#include "Plugins/SymbolFile/DWARF/LogChannelDWARF.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Host/linux/HostInfoLinux.h" +#include "lldb/Target/Platform.h" +#include "lldb/Utility/RegisterValue.h" + +namespace symbols_backend { + +void WasmPlatform::Initialize() { + lldb_private::Platform::SetHostPlatform( + std::make_shared(/*is_host_platform*/ true)); +} +void WasmPlatform::Terminate() {} + +WasmRegisters::WasmRegisters(lldb_private::Thread& thread, size_t frame_offset) + : RegisterContext(thread, 0), frame_offset_(frame_offset) { + fake_pc_register_.kinds[lldb::eRegisterKindGeneric] = LLDB_REGNUM_GENERIC_PC; +} + +const lldb_private::RegisterInfo* WasmRegisters::GetRegisterInfoAtIndex( + size_t reg) { + if (reg == 0) { + return &fake_pc_register_; + } + return nullptr; +} + +bool WasmRegisters::ReadRegister(const lldb_private::RegisterInfo* reg_info, + lldb_private::RegisterValue& reg_value) { + if (reg_info == &fake_pc_register_) { + reg_value = static_cast(frame_offset_); + return true; + } + return false; +} + +bool WasmUnwind::DoGetFrameInfoAtIndex(uint32_t frame_idx, + lldb::addr_t& cfa, + lldb::addr_t& pc, + bool& behaves_like_zeroth_frame) { + if (frame_idx != 0) { + return false; + } + pc = frame_offset_; + cfa = LLDB_INVALID_ADDRESS; + behaves_like_zeroth_frame = true; + return true; +} + +lldb::RegisterContextSP WasmThread::CreateRegisterContextForFrame( + lldb_private::StackFrame* frame) { + return unwind_.DoCreateRegisterContextForFrame(frame); +} + +lldb::RegisterContextSP WasmThread::GetRegisterContext() { + return unwind_.GetRegisterContext(); +} + +lldb::StackFrameSP WasmThread::GetFrame() { + if (!stack_frame_) { + stack_frame_ = this->GetStackFrameList()->GetFrameAtIndex(0); + this->SetSelectedFrame(stack_frame_.get()); + } + return stack_frame_; +} + +void WasmProcess::SetProxyAndFrameOffset(const api::DebuggerProxy& proxy, + size_t frame_offset) { + proxy_ = &proxy; + frame_offset_ = frame_offset; + this->SetPrivateState(lldb::StateType::eStateStopped); +} +bool WasmProcess::CanDebug(lldb::TargetSP target, + bool plugin_specified_by_name) { + return target->GetArchitecture().GetTriple().getArchName() == "wasm32"; +} +bool WasmProcess::DoUpdateThreadList( + lldb_private::ThreadList& old_thread_list, + lldb_private::ThreadList& new_thread_list) { + if (frame_offset_ > 0) { + new_thread_list.AddThread( + lldb::ThreadSP(new WasmThread(*this, frame_offset_))); + return true; + } + return false; +} + +size_t WasmProcess::DoReadMemory(lldb::addr_t vm_addr, + void* buf, + size_t size, + lldb_private::Status& error) { + if (!proxy_) { + error.SetErrorString("Proxy not initialized"); + return 0; + } + auto result = proxy_->ReadMemory(vm_addr, buf, size); + if (!result) { + error.SetErrorString(llvm::toString(result.takeError())); + return 0; + } + return *result; +} + +void WasmProcess::Initialize() { + lldb_private::PluginManager::RegisterPlugin( + GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance); +} + +lldb::ProcessSP WasmProcess::CreateInstance( + lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const lldb_private::FileSpec* crash_file_path, + bool can_connect) { + return lldb::ProcessSP(new WasmProcess(target_sp, listener_sp)); +} + +void WasmProcess::Terminate() { + lldb_private::PluginManager::UnregisterPlugin(CreateInstance); +} + +char SymbolFileWasmDWARF::ID; + +void SymbolFileWasmDWARF::Initialize() { + lldb_private::LogChannelDWARF::Initialize(); + lldb_private::PluginManager::RegisterPlugin( + GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance, + SymbolFileDWARF::DebuggerInitialize); +} + +void SymbolFileWasmDWARF::Terminate() { + lldb_private::PluginManager::UnregisterPlugin(CreateInstance); + lldb_private::LogChannelDWARF::Terminate(); +} + +llvm::StringRef SymbolFileWasmDWARF::GetPluginDescriptionStatic() { + return "Wasm DWARF"; +} + +lldb_private::SymbolFile* SymbolFileWasmDWARF::CreateInstance( + lldb::ObjectFileSP objfile_sp) { + return new SymbolFileWasmDWARF(std::move(objfile_sp), + /*dwo_section_list*/ nullptr); +} +} // namespace symbols_backend + +LLDB_PLUGIN_DEFINE_ADV(symbols_backend::SymbolFileWasmDWARF, + SymbolFileWasmDWARF) + +namespace lldb_private { +void HostInfoLinux::ComputeHostArchitectureSupport(ArchSpec& arch_32, + ArchSpec& arch_64) { + HostInfoPosix::ComputeHostArchitectureSupport(arch_32, arch_64); +} + +bool HostInfoLinux::ComputeSystemPluginsDirectory(FileSpec& file_spec) { + return false; +} + +bool HostInfoLinux::ComputeUserPluginsDirectory(FileSpec& file_spec) { + return false; +} + +Environment Host::GetEnvironment() { + return {}; +} +} // namespace lldb_private diff --git a/extensions/cxx_debugging/lib/WasmVendorPlugins.h b/extensions/cxx_debugging/lib/WasmVendorPlugins.h new file mode 100644 index 0000000..8f2829b --- /dev/null +++ b/extensions/cxx_debugging/lib/WasmVendorPlugins.h @@ -0,0 +1,329 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ +#define EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ +#include "ApiContext.h" +#include "Plugins/SymbolFile/DWARF/DWARFDeclContext.h" +#include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h" +#include "Plugins/TypeSystem/Clang/TypeSystemClang.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Target/RegisterContext.h" +#include "lldb/Target/Unwind.h" +#include "llvm/BinaryFormat/Dwarf.h" + +class SymbolVendorWASM; +class SymbolFileDWARF; + +namespace lldb_private { +class FileSystem; +class CPlusPlusLanguage; +class ClangASTContext; +namespace wasm { +class ObjectFileWasm; +class SymbolVendorWasm; +} // namespace wasm +} // namespace lldb_private + +namespace symbols_backend { + +template +static void initialize() { // NOLINT + T::Initialize(); +} +template +static void terminate() { // NOLINT + T::Terminate(); +} +template +static void initialize() { // NOLINT + T::Initialize(); + initialize(); +} +template +static void terminate() { // NOLINT + terminate(); + T::Terminate(); +} + +template +struct PluginRegistryContext { + PluginRegistryContext() { initialize(); } + ~PluginRegistryContext() { terminate(); } +}; + +class WasmPlatform : public lldb_private::Platform { + public: + using lldb_private::Platform::Platform; + + static void Initialize(); + + static void Terminate(); + + class Resolver : public lldb_private::UserIDResolver { + llvm::Optional DoGetUserName(id_t uid) final { return {}; } + llvm::Optional DoGetGroupName(id_t gid) final { return {}; } + }; + Resolver resolver_; + + llvm::StringRef GetPluginName() final { return "wasm32"; } + + llvm::StringRef GetDescription() final { return "wasm32"; } + + lldb_private::UserIDResolver& GetUserIDResolver() final { return resolver_; } + + std::vector GetSupportedArchitectures( + const lldb_private::ArchSpec& process_host_arch) final { + return {{lldb_private::ArchSpec("wasm32-unknown-unknown")}}; + } + + void CalculateTrapHandlerSymbolNames() final {} + + lldb::ProcessSP Attach(lldb_private::ProcessAttachInfo& attach_info, + lldb_private::Debugger& debugger, + lldb_private::Target* target, + lldb_private::Status& error) final { + error.SetErrorString("Cannot attach to processes"); + return {}; + } +}; + +class WasmRegisters : public lldb_private::RegisterContext { + using lldb_private::RegisterContext::RegisterContext; + lldb_private::RegisterInfo fake_pc_register_ = {"PC", + nullptr, + 4, + 0, + lldb::Encoding::eEncodingUint, + lldb::Format::eFormatDefault, + {0}, + nullptr, + nullptr}; + size_t frame_offset_; + + public: + WasmRegisters(lldb_private::Thread& thread, size_t frame_offset); + + void InvalidateAllRegisters() override{}; + + size_t GetRegisterCount() override { return 1; } + + const lldb_private::RegisterInfo* GetRegisterInfoAtIndex(size_t reg) override; + + size_t GetRegisterSetCount() override { return 0; } + + const lldb_private::RegisterSet* GetRegisterSet(size_t reg_set) override { + return nullptr; + } + + lldb::ByteOrder GetByteOrder() override { return lldb::eByteOrderLittle; } + + bool ReadRegister(const lldb_private::RegisterInfo* reg_info, + lldb_private::RegisterValue& reg_value) override; + + bool WriteRegister(const lldb_private::RegisterInfo* reg_info, + const lldb_private::RegisterValue& reg_value) override { + return false; + } + + friend class WasmThread; +}; + +class WasmUnwind : public lldb_private::Unwind { + size_t frame_offset_; + + public: + explicit WasmUnwind(lldb_private::Thread& thread, size_t frame_offset) + : Unwind(thread), frame_offset_(frame_offset) {} + void DoClear() final{}; + uint32_t DoGetFrameCount() final { return 1; } + + bool DoGetFrameInfoAtIndex(uint32_t frame_idx, + lldb::addr_t& cfa, + lldb::addr_t& pc, + bool& behaves_like_zeroth_frame) final; + + lldb::RegisterContextSP DoCreateRegisterContextForFrame( + lldb_private::StackFrame* frame) final { + return GetRegisterContext(); + } + lldb::RegisterContextSP GetRegisterContext() { + return lldb::RegisterContextSP{ + new WasmRegisters(GetThread(), frame_offset_)}; + } +}; + +class WasmThread : public lldb_private::Thread { + friend class WasmUnwind; + lldb::StackFrameSP stack_frame_; + WasmUnwind unwind_; + + public: + WasmThread(lldb_private::Process& process, size_t frame_offset) + : Thread(process, 0), unwind_(*this, frame_offset) {} + + void RefreshStateAfterStop() override {} + bool CalculateStopInfo() override { return false; } + + WasmUnwind& GetUnwinder() final { return unwind_; } + + lldb::RegisterContextSP CreateRegisterContextForFrame( + lldb_private::StackFrame* frame) override; + + lldb::RegisterContextSP GetRegisterContext() override; + + lldb::StackFrameSP GetFrame(); +}; + +class WasmProcess : public lldb_private::Process { + using lldb_private::Process::Process; + const api::DebuggerProxy* proxy_ = nullptr; + size_t frame_offset_ = 0; + + public: + static void Initialize(); + + static void Terminate(); + + static lldb::ProcessSP CreateInstance( + lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const lldb_private::FileSpec* crash_file_path, + bool can_connect); + + static llvm::StringRef GetPluginDescriptionStatic() { + return "wasm32 proces"; + } + + static llvm::StringRef GetPluginNameStatic() { return "wasm32"; } + + void SetProxyAndFrameOffset(const api::DebuggerProxy& proxy, + size_t frame_offset); + bool CanDebug(lldb::TargetSP target, bool plugin_specified_by_name) override; + lldb_private::Status DoDestroy() override { return {}; } + void RefreshStateAfterStop() override {} + bool DoUpdateThreadList(lldb_private::ThreadList& old_thread_list, + lldb_private::ThreadList& new_thread_list) override; + + size_t DoReadMemory(lldb::addr_t vm_addr, + void* buf, + size_t size, + lldb_private::Status& error) override; + + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } +}; + +class SymbolFileWasmDWARF : public ::SymbolFileDWARF { + static char ID; + + public: + using SymbolFileDWARF::SymbolFileDWARF; + + bool isA(const void* ClassID) const override { + return ClassID == &ID || SymbolFileDWARF::isA(ClassID); + } + static bool classof(const SymbolFile* obj) { return obj->isA(&ID); } + + static void Initialize(); + + static void Terminate(); + + static llvm::StringRef GetPluginNameStatic() { return "wasm_dwarf"; } + llvm::StringRef GetPluginName() final { return GetPluginNameStatic(); } + + static llvm::StringRef GetPluginDescriptionStatic(); + + static lldb_private::SymbolFile* CreateInstance( + lldb::ObjectFileSP objfile_sp); + + struct WasmValueLoader { + SymbolFileWasmDWARF& symbol_file; + explicit WasmValueLoader(SymbolFileWasmDWARF& symbol_file) + : symbol_file(symbol_file) { + assert(!symbol_file.current_value_loader_ && + "Cannot nest wasm eval contexts"); + symbol_file.current_value_loader_ = this; + } + ~WasmValueLoader() { symbol_file.current_value_loader_ = nullptr; } + + virtual llvm::Expected LoadWASMValue( + uint8_t storage_type, + const lldb_private::DataExtractor& data, + lldb::offset_t& offset) = 0; + }; + + lldb::offset_t GetVendorDWARFOpcodeSize( + const lldb_private::DataExtractor& data, + const lldb::offset_t data_offset, + const uint8_t op) const final { + return LLDB_INVALID_OFFSET; + } + + bool ParseVendorDWARFOpcode( + uint8_t op, + const lldb_private::DataExtractor& opcodes, + lldb::offset_t& offset, + std::vector& stack) const final { + if (!current_value_loader_) { + return false; + } + switch (op) { + case llvm::dwarf::DW_OP_WASM_location_int: + case llvm::dwarf::DW_OP_WASM_location: { + uint8_t storage_type = opcodes.GetU8(&offset); + auto value = + current_value_loader_->LoadWASMValue(storage_type, opcodes, offset); + if (!value) { + llvm::errs() << llvm::toString(value.takeError()); + return false; + } + auto stack_value = std::visit( + [](auto v) { return lldb_private::Scalar(v); }, value->value); + stack.push_back(stack_value); + stack.back().SetValueType(lldb_private::Value::ValueType::Scalar); + return true; + } + } + + return false; + } + + std::shared_ptr externref_type_sp; + + lldb::TypeSP FindDefinitionTypeForDWARFDeclContext( + const DWARFDeclContext& dwarf_decl_ctx) override { + // We define type externref_t as a 32-bit integer, so as to be + // able to transfer some information through the interpreter + const uint32_t dwarf_decl_ctx_count = dwarf_decl_ctx.GetSize(); + if (dwarf_decl_ctx_count > 0) { + const lldb_private::ConstString type_name(dwarf_decl_ctx[0].name); + if (type_name == "externref_t") { + if (!externref_type_sp) { + const lldb::LanguageType language = dwarf_decl_ctx.GetLanguage(); + auto type_system = GetTypeSystemForLanguage(language); + bool ok = !type_system.takeError(); + assert(ok); + auto ast = clang::dyn_cast( + type_system->get()); + lldb_private::CompilerType clang_type = + ast->GetBasicType(lldb::eBasicTypeUnsignedLongLong); + externref_type_sp = std::make_shared( + lldb::user_id_t(0), this, type_name, 4, nullptr, LLDB_INVALID_UID, + lldb_private::Type::eEncodingIsUID, lldb_private::Declaration(), + clang_type, lldb_private::Type::ResolveState::Forward); + } + return externref_type_sp; + } + } + return SymbolFileDWARF::FindDefinitionTypeForDWARFDeclContext( + dwarf_decl_ctx); + } + + private: + WasmValueLoader* current_value_loader_ = nullptr; +}; + +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ diff --git a/extensions/cxx_debugging/lib/api.h b/extensions/cxx_debugging/lib/api.h new file mode 100644 index 0000000..80ba820 --- /dev/null +++ b/extensions/cxx_debugging/lib/api.h @@ -0,0 +1,593 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// THIS IS GENERATED CODE, DO NOT MODIFY! +// clang-format off + +#ifndef EXTENSIONS_CXX_DEBUGGING_API_H_ +#define EXTENSIONS_CXX_DEBUGGING_API_H_ + +#include +#include +#include + +#include "llvm/ADT/Optional.h" + +namespace symbols_backend { +namespace api { +// Error details +class Error { + public: + enum class Code { + kInternalError, + kProtocolError, + kModuleNotFoundError, + kEvalError + }; + private: + Code code_ = {}; // An error code + std::string message_ = {}; // The error message + + public: + Error() = default; + virtual ~Error() = default; + Code GetCode() const { return code_; } + Error &SetCode(Code value) { + code_ = std::move(value); + return *this; + } + std::string GetMessage() const { return message_; } + Error &SetMessage(std::string value) { + message_ = std::move(value); + return *this; + } +}; + +// Offsets in raw modules +class RawLocationRange { + private: + std::string raw_module_id_ = {}; // Module identifier + int32_t start_offset_ = {}; // Start offset of the code range in the raw module + int32_t end_offset_ = {}; // Exclusive end offset of the code range in the raw module + + public: + RawLocationRange() = default; + virtual ~RawLocationRange() = default; + std::string GetRawModuleId() const { return raw_module_id_; } + RawLocationRange &SetRawModuleId(std::string value) { + raw_module_id_ = std::move(value); + return *this; + } + int32_t GetStartOffset() const { return start_offset_; } + RawLocationRange &SetStartOffset(int32_t value) { + start_offset_ = std::move(value); + return *this; + } + int32_t GetEndOffset() const { return end_offset_; } + RawLocationRange &SetEndOffset(int32_t value) { + end_offset_ = std::move(value); + return *this; + } +}; + +// Offsets in raw modules +class RawLocation { + private: + std::string raw_module_id_ = {}; // Module identifier + int32_t code_offset_ = {}; // Offset of the location in the raw module + int32_t inline_frame_index_ = {}; // Index of inline call frame, 0 is top. + + public: + RawLocation() = default; + virtual ~RawLocation() = default; + std::string GetRawModuleId() const { return raw_module_id_; } + RawLocation &SetRawModuleId(std::string value) { + raw_module_id_ = std::move(value); + return *this; + } + int32_t GetCodeOffset() const { return code_offset_; } + RawLocation &SetCodeOffset(int32_t value) { + code_offset_ = std::move(value); + return *this; + } + int32_t GetInlineFrameIndex() const { return inline_frame_index_; } + RawLocation &SetInlineFrameIndex(int32_t value) { + inline_frame_index_ = std::move(value); + return *this; + } +}; + +// Locations in source files +class SourceLocation { + private: + std::string raw_module_id_ = {}; // Module identifier + std::string source_file_ = {}; // Url of the source file + int32_t line_number_ = {}; // Line number of the location in the source file + int32_t column_number_ = {}; // Column number of the location in the source file + + public: + SourceLocation() = default; + virtual ~SourceLocation() = default; + std::string GetRawModuleId() const { return raw_module_id_; } + SourceLocation &SetRawModuleId(std::string value) { + raw_module_id_ = std::move(value); + return *this; + } + std::string GetSourceFile() const { return source_file_; } + SourceLocation &SetSourceFile(std::string value) { + source_file_ = std::move(value); + return *this; + } + int32_t GetLineNumber() const { return line_number_; } + SourceLocation &SetLineNumber(int32_t value) { + line_number_ = std::move(value); + return *this; + } + int32_t GetColumnNumber() const { return column_number_; } + SourceLocation &SetColumnNumber(int32_t value) { + column_number_ = std::move(value); + return *this; + } +}; + +// A source language variable +class Variable { + public: + enum class Scope { + kLocal, + kParameter, + kGlobal + }; + private: + Scope scope_ = {}; // Scope of the variable + std::string name_ = {}; // Name of the variable + std::string type_ = {}; // Type of the variable + std::vector typedefs_ = {}; + + public: + Variable() = default; + virtual ~Variable() = default; + Scope GetScope() const { return scope_; } + Variable &SetScope(Scope value) { + scope_ = std::move(value); + return *this; + } + std::string GetName() const { return name_; } + Variable &SetName(std::string value) { + name_ = std::move(value); + return *this; + } + std::string GetType() const { return type_; } + Variable &SetType(std::string value) { + type_ = std::move(value); + return *this; + } + std::vector GetTypedefs() const { return typedefs_; } + Variable &SetTypedefs(std::vector value) { + typedefs_ = std::move(value); + return *this; + } +}; + +class FieldInfo { + private: + llvm::Optional name_ = {}; + int32_t offset_ = {}; + std::string type_id_ = {}; + + public: + FieldInfo() = default; + virtual ~FieldInfo() = default; + llvm::Optional GetName() const { return name_; } + FieldInfo &SetName(llvm::Optional value) { + name_ = std::move(value); + return *this; + } + int32_t GetOffset() const { return offset_; } + FieldInfo &SetOffset(int32_t value) { + offset_ = std::move(value); + return *this; + } + std::string GetTypeId() const { return type_id_; } + FieldInfo &SetTypeId(std::string value) { + type_id_ = std::move(value); + return *this; + } +}; + +class Enumerator { + private: + std::string name_ = {}; + int64_t value_ = {}; + std::string type_id_ = {}; + + public: + Enumerator() = default; + virtual ~Enumerator() = default; + std::string GetName() const { return name_; } + Enumerator &SetName(std::string value) { + name_ = std::move(value); + return *this; + } + int64_t GetValue() const { return value_; } + Enumerator &SetValue(int64_t value) { + value_ = std::move(value); + return *this; + } + std::string GetTypeId() const { return type_id_; } + Enumerator &SetTypeId(std::string value) { + type_id_ = std::move(value); + return *this; + } +}; + +class TypeInfo { + private: + std::vector type_names_ = {}; + std::string type_id_ = {}; + int32_t alignment_ = {}; + int32_t size_ = {}; + bool can_expand_ = {}; + bool has_value_ = {}; + llvm::Optional array_size_ = {}; + bool is_pointer_ = {}; + std::vector members_ = {}; + std::vector enumerators_ = {}; + + public: + TypeInfo() = default; + virtual ~TypeInfo() = default; + std::vector GetTypeNames() const { return type_names_; } + TypeInfo &SetTypeNames(std::vector value) { + type_names_ = std::move(value); + return *this; + } + std::string GetTypeId() const { return type_id_; } + TypeInfo &SetTypeId(std::string value) { + type_id_ = std::move(value); + return *this; + } + int32_t GetAlignment() const { return alignment_; } + TypeInfo &SetAlignment(int32_t value) { + alignment_ = std::move(value); + return *this; + } + int32_t GetSize() const { return size_; } + TypeInfo &SetSize(int32_t value) { + size_ = std::move(value); + return *this; + } + bool GetCanExpand() const { return can_expand_; } + TypeInfo &SetCanExpand(bool value) { + can_expand_ = std::move(value); + return *this; + } + bool GetHasValue() const { return has_value_; } + TypeInfo &SetHasValue(bool value) { + has_value_ = std::move(value); + return *this; + } + llvm::Optional GetArraySize() const { return array_size_; } + TypeInfo &SetArraySize(llvm::Optional value) { + array_size_ = std::move(value); + return *this; + } + bool GetIsPointer() const { return is_pointer_; } + TypeInfo &SetIsPointer(bool value) { + is_pointer_ = std::move(value); + return *this; + } + std::vector GetMembers() const { return members_; } + TypeInfo &SetMembers(std::vector value) { + members_ = std::move(value); + return *this; + } + std::vector GetEnumerators() const { return enumerators_; } + TypeInfo &SetEnumerators(std::vector value) { + enumerators_ = std::move(value); + return *this; + } +}; + +// Return type of the AddRawModule command +class AddRawModuleResponse { + private: + std::vector sources_ = {}; // The original source files the raw module was compiled from. Filenames are in URL format + std::vector dwos_ = {}; // DWO filenames that might be lazily loaded. Used internally by the extension to set up emscripten filesystem. + llvm::Optional error_ = {}; // Error details if the raw module couldn't be handled + + public: + AddRawModuleResponse() = default; + virtual ~AddRawModuleResponse() = default; + std::vector GetSources() const { return sources_; } + AddRawModuleResponse &SetSources(std::vector value) { + sources_ = std::move(value); + return *this; + } + std::vector GetDwos() const { return dwos_; } + AddRawModuleResponse &SetDwos(std::vector value) { + dwos_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + AddRawModuleResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the SourceLocationToRawLocation command +class SourceLocationToRawLocationResponse { + private: + std::vector raw_location_ranges_ = {}; // The raw locations matching the source locations + llvm::Optional error_ = {}; // Error details if the command failed + + public: + SourceLocationToRawLocationResponse() = default; + virtual ~SourceLocationToRawLocationResponse() = default; + std::vector GetRawLocationRanges() const { return raw_location_ranges_; } + SourceLocationToRawLocationResponse &SetRawLocationRanges(std::vector value) { + raw_location_ranges_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + SourceLocationToRawLocationResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the RawLocationToSourceLocation command +class RawLocationToSourceLocationResponse { + private: + std::vector source_location_ = {}; // The source locations matching the raw locations + llvm::Optional error_ = {}; // Error details if the command failed + + public: + RawLocationToSourceLocationResponse() = default; + virtual ~RawLocationToSourceLocationResponse() = default; + std::vector GetSourceLocation() const { return source_location_; } + RawLocationToSourceLocationResponse &SetSourceLocation(std::vector value) { + source_location_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + RawLocationToSourceLocationResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the ListVariablesInScope command +class ListVariablesInScopeResponse { + private: + std::vector variable_ = {}; // The variables present in the scope + llvm::Optional error_ = {}; // Error details if the command failed + + public: + ListVariablesInScopeResponse() = default; + virtual ~ListVariablesInScopeResponse() = default; + std::vector GetVariable() const { return variable_; } + ListVariablesInScopeResponse &SetVariable(std::vector value) { + variable_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + ListVariablesInScopeResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the GetFunctionInfo command +class GetFunctionInfoResponse { + private: + std::vector function_names_ = {}; // A list of functions (multiple if inlined) starting with innermost. + std::vector missing_symbol_files_ = {}; // A string representing a missing .dwo file. + llvm::Optional error_ = {}; // error details if the command failed + + public: + GetFunctionInfoResponse() = default; + virtual ~GetFunctionInfoResponse() = default; + std::vector GetFunctionNames() const { return function_names_; } + GetFunctionInfoResponse &SetFunctionNames(std::vector value) { + function_names_ = std::move(value); + return *this; + } + std::vector GetMissingSymbolFiles() const { return missing_symbol_files_; } + GetFunctionInfoResponse &SetMissingSymbolFiles(std::vector value) { + missing_symbol_files_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + GetFunctionInfoResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the GetInlinedFunctionRanges command +class GetInlinedFunctionRangesResponse { + private: + std::vector raw_location_ranges_ = {}; // The raw locations of the inlined function or empty + llvm::Optional error_ = {}; // Error details if the command failed + + public: + GetInlinedFunctionRangesResponse() = default; + virtual ~GetInlinedFunctionRangesResponse() = default; + std::vector GetRawLocationRanges() const { return raw_location_ranges_; } + GetInlinedFunctionRangesResponse &SetRawLocationRanges(std::vector value) { + raw_location_ranges_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + GetInlinedFunctionRangesResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the GetInlinedCalleesRanges command +class GetInlinedCalleesRangesResponse { + private: + std::vector raw_location_ranges_ = {}; // The raw locations of any child inlined functions + llvm::Optional error_ = {}; // Error details if the command failed + + public: + GetInlinedCalleesRangesResponse() = default; + virtual ~GetInlinedCalleesRangesResponse() = default; + std::vector GetRawLocationRanges() const { return raw_location_ranges_; } + GetInlinedCalleesRangesResponse &SetRawLocationRanges(std::vector value) { + raw_location_ranges_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + GetInlinedCalleesRangesResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the GetMappedLines command +class GetMappedLinesResponse { + private: + std::vector _mapped_lines_ = {}; // Mapped lines + llvm::Optional error_ = {}; // Error details if the command failed + + public: + GetMappedLinesResponse() = default; + virtual ~GetMappedLinesResponse() = default; + std::vector GetMappedLines() const { return _mapped_lines_; } + GetMappedLinesResponse &SetMappedLines(std::vector value) { + _mapped_lines_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + GetMappedLinesResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + +// Return type of the EvaluateExpression command +class EvaluateExpressionResponse { + private: + std::vector type_infos_ = {}; + TypeInfo root_ = {}; + llvm::Optional display_value_ = {}; + llvm::Optional location_ = {}; + llvm::Optional memory_address_ = {}; + llvm::Optional> data_ = {}; + llvm::Optional error_ = {}; + + public: + EvaluateExpressionResponse() = default; + virtual ~EvaluateExpressionResponse() = default; + std::vector GetTypeInfos() const { return type_infos_; } + EvaluateExpressionResponse &SetTypeInfos(std::vector value) { + type_infos_ = std::move(value); + return *this; + } + TypeInfo GetRoot() const { return root_; } + EvaluateExpressionResponse &SetRoot(TypeInfo value) { + root_ = std::move(value); + return *this; + } + llvm::Optional GetDisplayValue() const { return display_value_; } + EvaluateExpressionResponse &SetDisplayValue(llvm::Optional value) { + display_value_ = std::move(value); + return *this; + } + llvm::Optional GetLocation() const { return location_; } + EvaluateExpressionResponse &SetLocation(llvm::Optional value) { + location_ = std::move(value); + return *this; + } + llvm::Optional GetMemoryAddress() const { return memory_address_; } + EvaluateExpressionResponse &SetMemoryAddress(llvm::Optional value) { + memory_address_ = std::move(value); + return *this; + } + llvm::Optional> GetData() const { return data_; } + EvaluateExpressionResponse &SetData(llvm::Optional> value) { + data_ = std::move(value); + return *this; + } + llvm::Optional GetError() const { return error_; } + EvaluateExpressionResponse &SetError(llvm::Optional value) { + error_ = std::move(value); + return *this; + } +}; + + +class DWARFSymbolsApi { + public: + + // Notify the plugin about a new script + virtual AddRawModuleResponse AddRawModule( + std::string raw_module_id, //A raw module identifier + std::string path //The path to the file with the wasm bytes + ) = 0; + + // Notify the plugin about a new script + virtual void RemoveRawModule( + std::string raw_module_id //The raw module identifier + ) = 0; + + // Find locations in raw modules from a location in a source file + virtual SourceLocationToRawLocationResponse SourceLocationToRawLocation( + std::string raw_module_id, //Module identifier + std::string source_file_url, //URL of the source file + int32_t line_number, //Line number of the location in the source file + int32_t column_number //Column number of the location in the source file + ) = 0; + + // Find locations in source files from a location in a raw module + virtual RawLocationToSourceLocationResponse RawLocationToSourceLocation( + std::string raw_module_id, //Module identifier + int32_t code_offset, //Offset of the location in the raw module + int32_t inline_frame_index //Index of inline frame + ) = 0; + + // List all variables in lexical scope at a location in a raw module + virtual ListVariablesInScopeResponse ListVariablesInScope( + std::string raw_module_id, //Module identifier + int32_t code_offset, //Offset of the location in the raw module + int32_t inline_frame_index //Index of inline frame + ) = 0; + + // Get function at location including inline frames. + virtual GetFunctionInfoResponse GetFunctionInfo( + std::string raw_module_id, //Module identifier + int32_t code_offset //Offset of the location in the raw module + ) = 0; + + // Get ranges for inlined function containing codeOffset. + virtual GetInlinedFunctionRangesResponse GetInlinedFunctionRanges( + std::string raw_module_id, //Module identifier + int32_t code_offset //Offset of the location in the raw module + ) = 0; + + // Get ranges for inlined functions called by function containing codeOffset. + virtual GetInlinedCalleesRangesResponse GetInlinedCalleesRanges( + std::string raw_module_id, //Module identifier + int32_t code_offset //Offset of the location in the raw module + ) = 0; + + virtual GetMappedLinesResponse GetMappedLines( + std::string raw_module_id, //Module identifier + std::string source_file_url //Source file URL + ) = 0; + + virtual EvaluateExpressionResponse EvaluateExpression( + RawLocation location, + std::string expression, + emscripten::val debug_proxy + ) = 0; +}; +} // namespace api +} // namespace symbols_backend + +#endif // EXTENSIONS_CXX_DEBUGGING_API_H_ diff --git a/extensions/cxx_debugging/src/CMakeLists.txt b/extensions/cxx_debugging/src/CMakeLists.txt new file mode 100644 index 0000000..6ee4c01 --- /dev/null +++ b/extensions/cxx_debugging/src/CMakeLists.txt @@ -0,0 +1,158 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +set(DEV_RESOURCES + TestDriver.js + DevToolsPluginForTests.html + index.html + ) + +set(EXTENSION_RESOURCES + DevToolsPlugin.html + ExtensionOptions.html + ) + +foreach(EXTENSION_RESOURCE IN LISTS EXTENSION_RESOURCES DEV_RESOURCES) + copy_file(${EXTENSION_RESOURCE} ${EXTENSION_RESOURCE}) +endforeach(EXTENSION_RESOURCE) +configure_file(manifest.json.in manifest.json @ONLY) + +set(EXTENSION_BUNDLE_ENTRYPOINTS + DevToolsPluginHost.js + DevToolsPluginWorkerMain.js + ExtensionOptions.js +) +if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") + set(EXTENSION_BUNDLE_SOURCEMAP TRUE) +else() + set(EXTENSION_BUNDLE_SOURCEMAP FALSE) +endif() +set(EXTENSION_BUNDLE_FORMAT "es") + +set(EXTENSION_BUNDLED_SOURCES) +foreach(entrypoint IN LISTS EXTENSION_BUNDLE_ENTRYPOINTS) + get_filename_component(NAME_WE ${entrypoint} NAME_WE) + list(APPEND EXTENSION_BUNDLED_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${NAME_WE}.bundle.js) +endforeach() + +configure_file(rollup.config.in.js rollup.config.in.js @ONLY) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.js + INPUT ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.in.js) + +add_custom_command( + OUTPUT + ${EXTENSION_BUNDLED_SOURCES} + COMMAND + ${CMAKE_COMMAND} + ARGS + -E env NODE_PATH=${PROJECT_SOURCE_DIR}/node_modules:${DEVTOOLS_SOURCE_DIR}/node_modules + python3 + ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py + --output + ${DEVTOOLS_SOURCE_DIR}/node_modules/.bin/rollup + -c --failAfterWarnings + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + MAIN_DEPENDENCY ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.js + DEPENDS ${TS_COMPILER_OUTPUTS} +) + +add_custom_target(Bundles DEPENDS ${EXTENSION_BUNDLED_SOURCES}) + +add_executable(SymbolsBackend SymbolsBackend.cc) +target_link_libraries(SymbolsBackend PUBLIC DWARFSymbols) +target_include_directories(SymbolsBackend PUBLIC + ${PROJECT_SOURCE_DIR}/third_party/emscripten/system/include) +target_link_libraries(SymbolsBackend PRIVATE -sMAXIMUM_MEMORY=4GB) +target_link_libraries(SymbolsBackend PRIVATE -sALLOW_MEMORY_GROWTH=1) +target_link_libraries(SymbolsBackend PRIVATE -sMODULARIZE=1) +target_link_libraries(SymbolsBackend PRIVATE -s'EXPORT_NAME=createSymbolsBackend') +target_link_libraries(SymbolsBackend PRIVATE -s'EXTRA_EXPORTED_RUNTIME_METHODS=[\"FS\"]') +target_link_libraries(SymbolsBackend PRIVATE -sERROR_ON_UNDEFINED_SYMBOLS=0) +target_link_libraries(SymbolsBackend PRIVATE --bind) +target_link_libraries(SymbolsBackend PRIVATE --no-heap-copy) +target_link_libraries(SymbolsBackend PRIVATE -sSEPARATE_DWARF_URL=SymbolsBackend.wasm.debug.wasm) +target_link_libraries(SymbolsBackend PRIVATE + -sWASM_BIGINT + -sDYNAMIC_EXECUTION=0 +) + +if (CXX_DEBUGGING_USE_SANITIZERS) + target_link_libraries(SymbolsBackend PRIVATE + -sINITIAL_MEMORY=1073741824 + -fsanitize=address,undefined + ) +endif() +if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") + target_link_libraries(SymbolsBackend PRIVATE + -sERROR_ON_WASM_CHANGES_AFTER_LINK + -sREVERSE_DEPS=all + -sASSERTIONS=1 + ) +else() + target_link_libraries(SymbolsBackend PRIVATE -sASSERTIONS=0) +endif() + +target_link_libraries(SymbolsBackend PRIVATE -s'ENVIRONMENT=web,worker') +target_link_libraries(SymbolsBackend PRIVATE -s'EXPORT_ES6=1') + +add_custom_command(OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp + COMMAND + ${LLVM_DWP} + ARGS + -e ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm + -o ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp + WORKING_DIRECTORY + ${PROJECT_BINARY_DIR} + DEPENDS + SymbolsBackend) + + option(CXX_DEBUGGING_DWO_ONLY "Do not build dwp when using split dwarf" OFF) + if(CXX_DEBUGGING_USE_SPLIT_DWARF) + if(CXX_DEBUGGING_DWO_ONLY) + if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp) + message(WARNING "File ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp exists from a previous build and will likely be used when debugging. This is probably not what you want.") + endif() + + file(CREATE_LINK ${PROJECT_BINARY_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/src SYMBOLIC) + file(CREATE_LINK ${PROJECT_BINARY_DIR}/lib ${CMAKE_CURRENT_BINARY_DIR}/lib SYMBOLIC) + file(CREATE_LINK ${PROJECT_BINARY_DIR}/third_party ${CMAKE_CURRENT_BINARY_DIR}/third_party SYMBOLIC) + else() + add_custom_target(DevToolsPluginDwp ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp) + endif() +endif() + +set(CXX_DEBUGGING_ARCHIVE "${PROJECT_BINARY_DIR}/cxx_debugging_extension-${CMAKE_PROJECT_VERSION}.zip") +set(CXX_DEBUGGING_INPUTS + ${EXTENSION_BUNDLED_SOURCES} + ${CMAKE_CURRENT_BINARY_DIR}/manifest.json + $) + +foreach(RESOURCE IN LISTS EXTENSION_RESOURCES) + list(APPEND CXX_DEBUGGING_INPUTS ${CMAKE_CURRENT_BINARY_DIR}/${RESOURCE}) +endforeach() + +set(CXX_DEBUGGING_DIST_FILES + ${CXX_DEBUGGING_INPUTS} + ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm +) +add_custom_command(OUTPUT "${CXX_DEBUGGING_ARCHIVE}" + COMMAND ${CMAKE_COMMAND} -E tar "cf" "${CXX_DEBUGGING_ARCHIVE}" --format=zip -- ${CXX_DEBUGGING_DIST_FILES} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CXX_DEBUGGING_INPUTS} Bundles + COMMENT "Zipping to ${CXX_DEBUGGING_ARCHIVE}.") + +if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_ARCHIVE} ${DEV_RESOURCES}) +else() + add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_INPUTS} ${DEV_RESOURCES}) +endif() + +add_custom_command(TARGET DevToolsPlugin POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CXX_DEBUGGING_GEN_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CXX_DEBUGGING_DIST_FILES} ${CXX_DEBUGGING_GEN_DIR} +) + +set(EXTENSION_BUNDLED_SOURCES ${EXTENSION_BUNDLED_SOURCES} PARENT_SCOPE) diff --git a/extensions/cxx_debugging/src/CustomFormatters.ts b/extensions/cxx_debugging/src/CustomFormatters.ts new file mode 100644 index 0000000..7c6ee1d --- /dev/null +++ b/extensions/cxx_debugging/src/CustomFormatters.ts @@ -0,0 +1,690 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; + +import type {WasmValue} from './WasmTypes.js'; +import type {HostInterface} from './WorkerRPC.js'; + +export interface FieldInfo { + typeId: string; + name: string|undefined; + offset: number; +} + +export interface Enumerator { + typeId: string; + name: string; + value: bigint; +} + +export interface TypeInfo { + typeId: string; + enumerators?: Enumerator[]; + alignment: number; + size: number; + isPointer: boolean; + members: FieldInfo[]; + arraySize: number; + hasValue: boolean; + typeNames: string[]; + canExpand: boolean; +} + +export interface WasmInterface { + readMemory(offset: number, length: number): Uint8Array; + getOp(op: number): WasmValue; + getLocal(local: number): WasmValue; + getGlobal(global: number): WasmValue; +} + +export interface Value { + location: number; + size: number; + typeNames: string[]; + asUint8: () => number; + asUint16: () => number; + asUint32: () => number; + asUint64: () => bigint; + asInt8: () => number; + asInt16: () => number; + asInt32: () => number; + asInt64: () => bigint; + asFloat32: () => number; + asFloat64: () => number; + asDataView: (offset?: number, size?: number) => DataView; + $: (member: string|number) => Value; + getMembers(): string[]; +} + +export class MemorySlice { + readonly begin: number; + buffer: ArrayBuffer; + + constructor(buffer: ArrayBuffer, begin: number) { + this.begin = begin; + this.buffer = buffer; + } + + merge(other: MemorySlice): MemorySlice { + if (other.begin < this.begin) { + return other.merge(this); + } + if (other.begin > this.end) { + throw new Error('Slices are not contiguous'); + } + if (other.end <= this.end) { + return this; + } + + const newBuffer = new Uint8Array(other.end - this.begin); + newBuffer.set(new Uint8Array(this.buffer), 0); + newBuffer.set(new Uint8Array(other.buffer, this.end - other.begin), this.length); + + return new MemorySlice(newBuffer.buffer, this.begin); + } + + contains(offset: number): boolean { + return this.begin <= offset && offset < this.end; + } + + get length(): number { + return this.buffer.byteLength; + } + + get end(): number { + return this.length + this.begin; + } + + view(begin: number, length: number): DataView { + return new DataView(this.buffer, begin - this.begin, length); + } +} + +export class PageStore { + readonly slices: MemorySlice[] = []; + + // Returns the highest index |i| such that |slices[i].start <= offset|, or -1 if there is no such |i|. + findSliceIndex(offset: number): number { + let begin = 0; + let end = this.slices.length; + while (begin < end) { + const idx = Math.floor((end + begin) / 2); + + const pivot = this.slices[idx]; + if (offset < pivot.begin) { + end = idx; + } else { + begin = idx + 1; + } + } + return begin - 1; + } + + findSlice(offset: number): MemorySlice|null { + return this.getSlice(this.findSliceIndex(offset), offset); + } + + private getSlice(index: number, offset: number): MemorySlice|null { + if (index < 0) { + return null; + } + const candidate = this.slices[index]; + return candidate?.contains(offset) ? candidate : null; + } + + addSlice(buffer: ArrayBuffer|number[], begin: number): MemorySlice { + let slice = new MemorySlice(Array.isArray(buffer) ? new Uint8Array(buffer).buffer : buffer, begin); + + let leftPosition = this.findSliceIndex(slice.begin - 1); + const leftOverlap = this.getSlice(leftPosition, slice.begin - 1); + if (leftOverlap) { + slice = slice.merge(leftOverlap); + } else { + leftPosition++; + } + const rightPosition = this.findSliceIndex(slice.end); + const rightOverlap = this.getSlice(rightPosition, slice.end); + if (rightOverlap) { + slice = slice.merge(rightOverlap); + } + this.slices.splice( + leftPosition, // Insert to the right if no overlap + rightPosition - leftPosition + 1, // Delete one additional slice if overlapping on the left + slice); + return slice; + } +} +export class WasmMemoryView { + private readonly wasm: WasmInterface; + private readonly pages = new PageStore(); + private static readonly PAGE_SIZE = 4096; + + constructor(wasm: WasmInterface) { + this.wasm = wasm; + } + + private page(byteOffset: number, byteLength: number): {page: number, offset: number, count: number} { + const mask = WasmMemoryView.PAGE_SIZE - 1; + const offset = byteOffset & mask; + const page = byteOffset - offset; + const rangeEnd = byteOffset + byteLength; + const count = 1 + Math.ceil((rangeEnd - (rangeEnd & mask) - page) / WasmMemoryView.PAGE_SIZE); + return {page, offset, count}; + } + + private getPages(page: number, count: number): DataView { + if (page & (WasmMemoryView.PAGE_SIZE - 1)) { + throw new Error('Not a valid page'); + } + let slice = this.pages.findSlice(page); + const size = WasmMemoryView.PAGE_SIZE * count; + if (!slice || slice.length < count * WasmMemoryView.PAGE_SIZE) { + const data = this.wasm.readMemory(page, size); + if (data.byteOffset !== 0 || data.byteLength !== data.buffer.byteLength) { + throw new Error('Did not expect a partial memory view'); + } + slice = this.pages.addSlice(data.buffer, page); + } + return slice.view(page, size); + } + + getFloat32(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 4); + const view = this.getPages(page, count); + return view.getFloat32(offset, littleEndian); + } + getFloat64(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 8); + const view = this.getPages(page, count); + return view.getFloat64(offset, littleEndian); + } + getInt8(byteOffset: number): number { + const {offset, page, count} = this.page(byteOffset, 1); + const view = this.getPages(page, count); + return view.getInt8(offset); + } + getInt16(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 2); + const view = this.getPages(page, count); + return view.getInt16(offset, littleEndian); + } + getInt32(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 4); + const view = this.getPages(page, count); + return view.getInt32(offset, littleEndian); + } + getUint8(byteOffset: number): number { + const {offset, page, count} = this.page(byteOffset, 1); + const view = this.getPages(page, count); + return view.getUint8(offset); + } + getUint16(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 2); + const view = this.getPages(page, count); + return view.getUint16(offset, littleEndian); + } + getUint32(byteOffset: number, littleEndian?: boolean): number { + const {offset, page, count} = this.page(byteOffset, 4); + const view = this.getPages(page, count); + return view.getUint32(offset, littleEndian); + } + getBigInt64(byteOffset: number, littleEndian?: boolean): bigint { + const {offset, page, count} = this.page(byteOffset, 8); + const view = this.getPages(page, count); + return view.getBigInt64(offset, littleEndian); + } + getBigUint64(byteOffset: number, littleEndian?: boolean): bigint { + const {offset, page, count} = this.page(byteOffset, 8); + const view = this.getPages(page, count); + return view.getBigUint64(offset, littleEndian); + } + asDataView(byteOffset: number, byteLength: number): DataView { + const {offset, page, count} = this.page(byteOffset, byteLength); + const view = this.getPages(page, count); + return new DataView(view.buffer, view.byteOffset + offset, byteLength); + } +} + +export class CXXValue implements Value, LazyObject { + readonly location: number; + private readonly type: TypeInfo; + private readonly data?: number[]; + private readonly memoryOrDataView: DataView|WasmMemoryView; + private readonly wasm: WasmInterface; + private readonly typeMap: Map; + private readonly memoryView: WasmMemoryView; + private membersMap?: Map; + private readonly objectStore: LazyObjectStore; + private readonly objectId: string; + private readonly displayValue: string|undefined; + private readonly memoryAddress?: number; + + constructor( + objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, location: number, type: TypeInfo, + typeMap: Map, data?: number[], displayValue?: string, memoryAddress?: number) { + if (!location && !data) { + throw new Error('Cannot represent nullptr'); + } + this.data = data; + this.location = location; + this.type = type; + this.typeMap = typeMap; + this.wasm = wasm; + this.memoryOrDataView = data ? new DataView(new Uint8Array(data).buffer) : memoryView; + if (data && data.length !== type.size) { + throw new Error('Invalid data size'); + } + this.memoryView = memoryView; + this.objectStore = objectStore; + this.objectId = objectStore.store(this); + this.displayValue = displayValue; + this.memoryAddress = memoryAddress; + } + + static create(objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, typeInfo: { + typeInfos: TypeInfo[], + root: TypeInfo, + location?: number, + data?: number[], + displayValue?: string, + memoryAddress?: number, + }): CXXValue { + const typeMap = new Map(); + for (const info of typeInfo.typeInfos) { + typeMap.set(info.typeId, info); + } + const {location, root, data, displayValue, memoryAddress} = typeInfo; + return new CXXValue(objectStore, wasm, memoryView, location ?? 0, root, typeMap, data, displayValue, memoryAddress); + } + + private get members(): Map { + if (!this.membersMap) { + this.membersMap = new Map(); + for (const member of this.type.members) { + const memberType = this.typeMap.get(member.typeId); + if (memberType && member.name) { + const memberLocation = member.name === '*' ? this.memoryOrDataView.getUint32(this.location, true) : + this.location + member.offset; + this.membersMap.set(member.name, {location: memberLocation, type: memberType}); + } + } + } + return this.membersMap; + } + + private getArrayElement(index: number): CXXValue { + const data = this.members.has('*') ? undefined : this.data; + const element = this.members.get('*') || this.members.get('0'); + if (!element) { + throw new Error(`Incomplete type information for array or pointer type '${this.typeNames}'`); + } + return new CXXValue( + this.objectStore, this.wasm, this.memoryView, element.location + index * element.type.size, element.type, + this.typeMap, data); + } + + async getProperties(): Promise> { + const properties = []; + if (this.type.arraySize > 0) { + for (let index = 0; index < this.type.arraySize; ++index) { + properties.push({name: `${index}`, property: await this.getArrayElement(index)}); + } + } else { + const members = await this.members; + const data = members.has('*') ? undefined : this.data; + for (const [name, {location, type}] of members) { + const property = new CXXValue(this.objectStore, this.wasm, this.memoryView, location, type, this.typeMap, data); + properties.push({name, property}); + } + } + return properties; + } + + async asRemoteObject(): Promise { + if (this.type.hasValue && this.type.arraySize === 0) { + const formatter = CustomFormatters.get(this.type); + if (!formatter) { + const type = 'undefined' as Chrome.DevTools.RemoteObjectType; + const description = ''; + return {type, description, hasChildren: false}; + } + + if (this.location === undefined || (!this.data && this.location === 0xffffffff)) { + const type = 'undefined' as Chrome.DevTools.RemoteObjectType; + const description = ''; + return {type, description, hasChildren: false}; + } + const value = + new CXXValue(this.objectStore, this.wasm, this.memoryView, this.location, this.type, this.typeMap, this.data); + + try { + const formattedValue = await formatter.format(this.wasm, value); + return await lazyObjectFromAny( + formattedValue, this.objectStore, this.type, this.displayValue, this.memoryAddress) + .asRemoteObject(); + } catch { + // Fallthrough + } + } + + const type = (this.type.arraySize > 0 ? 'array' : 'object') as Chrome.DevTools.RemoteObjectType; + const {objectId} = this; + return { + type, + description: this.type.typeNames[0], + hasChildren: this.type.members.length > 0, + linearMemoryAddress: this.memoryAddress, + linearMemorySize: this.type.size, + objectId, + }; + } + + get typeNames(): string[] { + return this.type.typeNames; + } + + get size(): number { + return this.type.size; + } + + asInt8(): number { + return this.memoryOrDataView.getInt8(this.location); + } + asInt16(): number { + return this.memoryOrDataView.getInt16(this.location, true); + } + asInt32(): number { + return this.memoryOrDataView.getInt32(this.location, true); + } + asInt64(): bigint { + return this.memoryOrDataView.getBigInt64(this.location, true); + } + asUint8(): number { + return this.memoryOrDataView.getUint8(this.location); + } + asUint16(): number { + return this.memoryOrDataView.getUint16(this.location, true); + } + asUint32(): number { + return this.memoryOrDataView.getUint32(this.location, true); + } + asUint64(): bigint { + return this.memoryOrDataView.getBigUint64(this.location, true); + } + asFloat32(): number { + return this.memoryOrDataView.getFloat32(this.location, true); + } + asFloat64(): number { + return this.memoryOrDataView.getFloat64(this.location, true); + } + asDataView(offset?: number, size?: number): DataView { + offset = this.location + (offset ?? 0); + size = size ?? this.size; + if (this.memoryOrDataView instanceof DataView) { + size = Math.min(size - offset, this.memoryOrDataView.byteLength - offset - this.location); + if (size < 0) { + throw new RangeError('Size exceeds the buffer range'); + } + return new DataView( + this.memoryOrDataView.buffer, this.memoryOrDataView.byteOffset + this.location + offset, size); + } + return this.memoryView.asDataView(offset, size); + } + $(selector: string|number): CXXValue { + const data = this.members.has('*') ? undefined : this.data; + + if (typeof selector === 'number') { + return this.getArrayElement(selector); + } + + const dot = selector.indexOf('.'); + const memberName = dot >= 0 ? selector.substring(0, dot) : selector; + selector = selector.substring(memberName.length + 1); + + const member = this.members.get(memberName); + if (!member) { + throw new Error(`Type ${this.typeNames[0] || ''} has no member '${ + memberName}'. Available members are: ${Array.from(this.members.keys())}`); + } + const memberValue = + new CXXValue(this.objectStore, this.wasm, this.memoryView, member.location, member.type, this.typeMap, data); + if (selector.length === 0) { + return memberValue; + } + return memberValue.$(selector); + } + + getMembers(): string[] { + return Array.from(this.members.keys()); + } +} + +export interface LazyObject { + getProperties(): Promise>; + asRemoteObject(): Promise; +} + +export function primitiveObject( + value: T, description?: string, linearMemoryAddress?: number, type?: TypeInfo): PrimitiveLazyObject|null { + if (['number', 'string', 'boolean', 'bigint', 'undefined'].includes(typeof value)) { + if (typeof value === 'bigint' || typeof value === 'number') { + const enumerator = type?.enumerators?.find(e => e.value === BigInt(value)); + if (enumerator) { + description = enumerator.name; + } + } + return new PrimitiveLazyObject( + typeof value as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress, type?.size); + } + return null; +} + +function lazyObjectFromAny( + value: FormatterResult, objectStore: LazyObjectStore, type?: TypeInfo, description?: string, + linearMemoryAddress?: number): LazyObject { + const primitive = primitiveObject(value, description, linearMemoryAddress, type); + if (primitive) { + return primitive; + } + if (value instanceof CXXValue) { + return value; + } + if (typeof value === 'object') { + if (value === null) { + return new PrimitiveLazyObject( + 'null' as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress); + } + return new LocalLazyObject(value, objectStore, type, linearMemoryAddress); + } + if (typeof value === 'function') { + return value(); + } + + throw new Error('Value type is not formattable'); +} + +export class LazyObjectStore { + private nextObjectId = 0; + private objects = new Map(); + + store(lazyObject: LazyObject): string { + const objectId = `${this.nextObjectId++}`; + this.objects.set(objectId, lazyObject); + return objectId; + } + + get(objectId: string): LazyObject|undefined { + return this.objects.get(objectId); + } + + release(objectId: string): void { + this.objects.delete(objectId); + } + + clear(): void { + this.objects.clear(); + } +} + +export class PrimitiveLazyObject implements LazyObject { + readonly type: Chrome.DevTools.RemoteObjectType; + readonly value: T; + readonly description: string; + private readonly linearMemoryAddress?: number; + private readonly linearMemorySize?: number; + constructor( + type: Chrome.DevTools.RemoteObjectType, value: T, description?: string, linearMemoryAddress?: number, + linearMemorySize?: number) { + this.type = type; + this.value = value; + this.description = description ?? `${value}`; + this.linearMemoryAddress = linearMemoryAddress; + this.linearMemorySize = linearMemorySize; + } + + async getProperties(): Promise> { + return []; + } + + async asRemoteObject(): Promise { + const {type, value, description, linearMemoryAddress, linearMemorySize} = this; + return {type, hasChildren: false, value, description, linearMemoryAddress, linearMemorySize}; + } +} + +export class LocalLazyObject implements LazyObject { + readonly value: Object; + private readonly objectId; + private readonly objectStore: LazyObjectStore; + private readonly type?: TypeInfo; + private readonly linearMemoryAddress?: number; + + constructor(value: object, objectStore: LazyObjectStore, type?: TypeInfo, linearMemoryAddress?: number) { + this.value = value; + this.objectStore = objectStore; + this.objectId = objectStore.store(this); + this.type = type; + this.linearMemoryAddress = linearMemoryAddress; + } + + async getProperties(): Promise> { + return Object.entries(this.value).map(([name, value]) => { + const property = lazyObjectFromAny(value, this.objectStore); + return {name, property}; + }); + } + + async asRemoteObject(): Promise { + const type = (Array.isArray(this.value) ? 'array' : 'object') as Chrome.DevTools.RemoteObjectType; + const {objectId, type: valueType, linearMemoryAddress} = this; + return { + type, + objectId, + description: valueType?.typeNames[0], + hasChildren: Object.keys(this.value).length > 0, + linearMemorySize: valueType?.size, + linearMemoryAddress, + }; + } +} + +export type FormatterResult = number|string|boolean|bigint|undefined|CXXValue|object|(() => LazyObject); +export type FormatterCallback = (wasm: WasmInterface, value: Value) => FormatterResult; +export interface Formatter { + types: string[]|((t: TypeInfo) => boolean); + imports?: FormatterCallback[]; + format: FormatterCallback; +} + +export class HostWasmInterface { + private readonly hostInterface: HostInterface; + private readonly stopId: unknown; + private readonly cache: Chrome.DevTools.ForeignObject[] = []; + readonly view: WasmMemoryView; + constructor(hostInterface: HostInterface, stopId: unknown) { + this.hostInterface = hostInterface; + this.stopId = stopId; + this.view = new WasmMemoryView(this); + } + readMemory(offset: number, length: number): Uint8Array { + return new Uint8Array(this.hostInterface.getWasmLinearMemory(offset, length, this.stopId)); + } + getOp(op: number): WasmValue { + return this.hostInterface.getWasmOp(op, this.stopId); + } + getLocal(local: number): WasmValue { + return this.hostInterface.getWasmLocal(local, this.stopId); + } + getGlobal(global: number): WasmValue { + return this.hostInterface.getWasmGlobal(global, this.stopId); + } +} + +export class DebuggerProxy { + wasm: HostWasmInterface; + target: EmscriptenModule; + constructor(wasm: HostWasmInterface, target: EmscriptenModule) { + this.wasm = wasm; + this.target = target; + } + + readMemory(src: number, dst: number, length: number): number { + const data = this.wasm.view.asDataView(src, length); + this.target.HEAP8.set(new Uint8Array(data.buffer, data.byteOffset, length), dst); + return data.byteLength; + } + getLocal(index: number): WasmValue { + return this.wasm.getLocal(index); + } + getGlobal(index: number): WasmValue { + return this.wasm.getGlobal(index); + } + getOperand(index: number): WasmValue { + return this.wasm.getOp(index); + } +} + +export class CustomFormatters { + private static formatters = new Map(); + private static genericFormatters: Formatter[] = []; + + static addFormatter(formatter: Formatter): void { + if (Array.isArray(formatter.types)) { + for (const type of formatter.types) { + CustomFormatters.formatters.set(type, formatter); + } + } else { + CustomFormatters.genericFormatters.push(formatter); + } + } + + static get(type: TypeInfo): Formatter|null { + for (const name of type.typeNames) { + const formatter = CustomFormatters.formatters.get(name); + if (formatter) { + return formatter; + } + } + + for (const t of type.typeNames) { + const CONST_PREFIX = 'const '; + if (t.startsWith(CONST_PREFIX)) { + const formatter = CustomFormatters.formatters.get(t.substr(CONST_PREFIX.length)); + if (formatter) { + return formatter; + } + } + } + + for (const formatter of CustomFormatters.genericFormatters) { + if (formatter.types instanceof Function) { + if (formatter.types(type)) { + return formatter; + } + } + } + return null; + } +} diff --git a/extensions/cxx_debugging/src/DWARFSymbols.ts b/extensions/cxx_debugging/src/DWARFSymbols.ts new file mode 100644 index 0000000..01ab958 --- /dev/null +++ b/extensions/cxx_debugging/src/DWARFSymbols.ts @@ -0,0 +1,615 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import './Formatters.js'; + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; + +import * as Formatters from './CustomFormatters.js'; +import { + DEFAULT_MODULE_CONFIGURATIONS, + findModuleConfiguration, + type ModuleConfigurations, + resolveSourcePathToURL, +} from './ModuleConfiguration.js'; +import type * as SymbolsBackend from './SymbolsBackend.js'; +import createSymbolsBackend from './SymbolsBackend.js'; +import type {HostInterface} from './WorkerRPC.js'; + +function mapVector(vector: SymbolsBackend.Vector, callback: (apiElement: ApiT) => T): T[] { + const elements: T[] = []; + for (let i = 0; i < vector.size(); ++i) { + const element = vector.get(i); + elements.push(callback(element)); + } + return elements; +} + +interface ScopeInfo { + type: 'GLOBAL'|'LOCAL'|'PARAMETER'; + typeName: string; + icon?: string; +} + +type LazyFSNode = FS.FSNode&{contents: {cacheLength: () => void, length: number}}; + +function mapEnumerator(apiEnumerator: SymbolsBackend.Enumerator): Formatters.Enumerator { + return {typeId: apiEnumerator.typeId, value: apiEnumerator.value, name: apiEnumerator.name}; +} + +function mapFieldInfo(apiFieldInfo: SymbolsBackend.FieldInfo): Formatters.FieldInfo { + return {typeId: apiFieldInfo.typeId, offset: apiFieldInfo.offset, name: apiFieldInfo.name}; +} + +class ModuleInfo { + readonly fileNameToUrl: Map; + readonly urlToFileName: Map; + readonly dwarfSymbolsPlugin: SymbolsBackend.DWARFSymbolsPlugin; + + constructor( + readonly symbolsUrl: string, readonly symbolsFileName: string, readonly symbolsDwpFileName: string|undefined, + readonly backend: SymbolsBackend.Module) { + this.fileNameToUrl = new Map(); + this.urlToFileName = new Map(); + this.dwarfSymbolsPlugin = new backend.DWARFSymbolsPlugin(); + } + + stringifyScope(scope: SymbolsBackend.VariableScope): 'GLOBAL'|'LOCAL'|'PARAMETER' { + switch (scope) { + case this.backend.VariableScope.GLOBAL: + return 'GLOBAL'; + case this.backend.VariableScope.LOCAL: + return 'LOCAL'; + case this.backend.VariableScope.PARAMETER: + return 'PARAMETER'; + } + throw new Error(`InternalError: Invalid scope ${scope}`); + } + + stringifyErrorCode(errorCode: SymbolsBackend.ErrorCode): string { + switch (errorCode) { + case this.backend.ErrorCode.PROTOCOL_ERROR: + return 'ProtocolError:'; + case this.backend.ErrorCode.MODULE_NOT_FOUND_ERROR: + return 'ModuleNotFoundError:'; + case this.backend.ErrorCode.INTERNAL_ERROR: + return 'InternalError'; + case this.backend.ErrorCode.EVAL_ERROR: + return 'EvalError'; + } + throw new Error(`InternalError: Invalid error code ${errorCode}`); + } +} + +export function createEmbindPool(): { + flush(): void, + manage(object: T): T, + unmanage(object: T): boolean, +} { + class EmbindObjectPool { + private objectPool: SymbolsBackend.EmbindObject[] = []; + + flush(): void { + for (const object of this.objectPool.reverse()) { + object.delete(); + } + this.objectPool = []; + } + + manage(object: T): T { + if (typeof object !== 'undefined') { + this.objectPool.push(object); + } + return object; + } + + unmanage(object: T): boolean { + const index = this.objectPool.indexOf(object); + if (index > -1) { + this.objectPool.splice(index, 1); + object.delete(); + return true; + } + return false; + } + } + + const pool = new EmbindObjectPool(); + const manage = pool.manage.bind(pool); + const unmanage = pool.unmanage.bind(pool); + const flush = pool.flush.bind(pool); + return {manage, unmanage, flush}; +} + +// Cache the underlying WebAssembly module after the first instantiation +// so that subsequent calls to `createSymbolsBackend()` are faster, which +// greatly speeds up the test suite. +let symbolsBackendModulePromise: undefined|Promise; +function instantiateWasm( + imports: WebAssembly.Imports, + callback: (module: WebAssembly.Module) => void, + resourceLoader: ResourceLoader, + ): Emscripten.WebAssemblyExports { + if (!symbolsBackendModulePromise) { + symbolsBackendModulePromise = resourceLoader.createSymbolsBackendModulePromise(); + } + symbolsBackendModulePromise.then(module => WebAssembly.instantiate(module, imports)) + .then(callback) + .catch(console.error); + return []; +} + +export type RawModule = Chrome.DevTools.RawModule&{dwp?: ArrayBuffer}; + +export interface ResourceLoader { + loadSymbols(rawModuleId: string, rawModule: RawModule, url: URL, filesystem: typeof FS, hostInterface: HostInterface): + Promise<{symbolsFileName: string, symbolsDwpFileName?: string}>; + createSymbolsBackendModulePromise(): Promise; + possiblyMissingSymbols?: string[]; +} + +export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExtensionPlugin { + private moduleInfos = new Map>(); + private lazyObjects = new Formatters.LazyObjectStore(); + + constructor( + readonly moduleConfigurations: ModuleConfigurations, readonly resourceLoader: ResourceLoader, + readonly hostInterface: HostInterface) { + this.moduleConfigurations = moduleConfigurations; + } + + private async newModuleInfo(rawModuleId: string, symbolsHint: string, rawModule: RawModule): Promise { + const {flush, manage} = createEmbindPool(); + try { + const rawModuleURL = new URL(rawModule.url); + const {pathSubstitutions} = findModuleConfiguration(this.moduleConfigurations, rawModuleURL); + const symbolsURL = symbolsHint ? resolveSourcePathToURL([], symbolsHint, rawModuleURL) : rawModuleURL; + + const instantiateWasmWrapper = + (imports: Emscripten.WebAssemblyImports, + callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { + // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. + return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); + }; + const backend = await createSymbolsBackend({instantiateWasm: instantiateWasmWrapper}); + const {symbolsFileName, symbolsDwpFileName} = + await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); + const moduleInfo = new ModuleInfo(symbolsURL.href, symbolsFileName, symbolsDwpFileName, backend); + + const addRawModuleResponse = manage(moduleInfo.dwarfSymbolsPlugin.AddRawModule(rawModuleId, symbolsFileName)); + mapVector(manage(addRawModuleResponse.sources), fileName => { + const fileURL = resolveSourcePathToURL(pathSubstitutions, fileName, symbolsURL); + moduleInfo.fileNameToUrl.set(fileName, fileURL.href); + moduleInfo.urlToFileName.set(fileURL.href, fileName); + }); + + // Set up lazy dwo files if we are running on a worker + if (typeof global === 'undefined' && typeof importScripts === 'function' && + typeof XMLHttpRequest !== 'undefined') { + mapVector(manage(addRawModuleResponse.dwos), dwoFile => { + const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; + const pathSplit = absolutePath.split('/'); + const fileName = pathSplit.pop() as string; + const parentDirectory = pathSplit.join('/'); + + // Sometimes these stick around. + try { + backend.FS.unlink(absolutePath); + } catch { + } + // Ensure directory exists + if (parentDirectory.length > 1) { + // TypeScript doesn't know about createPath + // @ts-expect-error doesn't exit on types + backend.FS.createPath('/', parentDirectory.substring(1), true, true); + } + + const dwoURL = new URL(dwoFile, symbolsURL).href; + const node = backend.FS.createLazyFile(parentDirectory, fileName, dwoURL, true, false) as LazyFSNode; + const cacheLength = node.contents.cacheLength; + const wrapper = (): void => { + try { + cacheLength.apply(node.contents); + void this.hostInterface.reportResourceLoad(dwoURL, {success: true, size: node.contents.length}); + } catch (e) { + void this.hostInterface.reportResourceLoad(dwoURL, {success: false, errorMessage: (e as Error).message}); + // Rethrow any error fetching the content as errno 44 (EEXIST) + // TypeScript doesn't know about the ErrnoError constructor + // @ts-expect-error doesn't exit on types + throw new backend.FS.ErrnoError(44); + } + }; + node.contents.cacheLength = wrapper; + }); + } + + return moduleInfo; + } finally { + flush(); + } + } + + async addRawModule(rawModuleId: string, symbolsUrl: string, rawModule: RawModule): Promise { + // This complex logic makes sure that addRawModule / removeRawModule calls are + // handled sequentially for the same rawModuleId, and thus this looks symmetrical + // to the removeRawModule() method below. The idea is that we chain our operation + // on any previous operation for the same rawModuleId, and thereby end up with a + // single sequence of events. + const originalPromise = Promise.resolve(this.moduleInfos.get(rawModuleId)); + const moduleInfoPromise = originalPromise.then(moduleInfo => { + if (moduleInfo) { + throw new Error(`InternalError: Duplicate module with ID '${rawModuleId}'`); + } + return this.newModuleInfo(rawModuleId, symbolsUrl, rawModule); + }); + // This looks a bit odd, but it's important that the operation is chained via + // the `_moduleInfos` map *and* at the same time resolves to it's original + // value in case of an error (i.e. if someone tried to add the same rawModuleId + // twice, this will retain the original value in that case instead of having all + // users get the internal error). + this.moduleInfos.set(rawModuleId, moduleInfoPromise.catch(() => originalPromise)); + const moduleInfo = await moduleInfoPromise; + return [...moduleInfo.urlToFileName.keys()]; + } + + private async getModuleInfo(rawModuleId: string): Promise { + const moduleInfo = await this.moduleInfos.get(rawModuleId); + if (!moduleInfo) { + throw new Error(`InternalError: Unknown module with raw module ID ${rawModuleId}`); + } + return moduleInfo; + } + + async removeRawModule(rawModuleId: string): Promise { + const originalPromise = Promise.resolve(this.moduleInfos.get(rawModuleId)); + const moduleInfoPromise = originalPromise.then(moduleInfo => { + if (!moduleInfo) { + throw new Error(`InternalError: No module with ID '${rawModuleId}'`); + } + return undefined; + }); + this.moduleInfos.set(rawModuleId, moduleInfoPromise.catch(() => originalPromise)); + await moduleInfoPromise; + } + + async sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): + Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(sourceLocation.rawModuleId); + const sourceFile = moduleInfo.urlToFileName.get(sourceLocation.sourceFileURL); + if (!sourceFile) { + throw new Error(`InternalError: Unknown URL ${sourceLocation.sourceFileURL}`); + } + try { + const rawLocations = manage(moduleInfo.dwarfSymbolsPlugin.SourceLocationToRawLocation( + sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber)); + const error = manage(rawLocations.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { + const {rawModuleId, startOffset, endOffset} = manage(rawLocation); + return {rawModuleId, startOffset, endOffset}; + }); + return locations; + } finally { + flush(); + } + } + + async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): + Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); + try { + const sourceLocations = moduleInfo.dwarfSymbolsPlugin.RawLocationToSourceLocation( + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); + const error = manage(sourceLocations.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const locations = mapVector(manage(sourceLocations.sourceLocation), sourceLocation => { + const sourceFileURL = moduleInfo.fileNameToUrl.get(sourceLocation.sourceFile); + if (!sourceFileURL) { + throw new Error(`InternalError: Unknown source file ${sourceLocation.sourceFile}`); + } + const {rawModuleId, lineNumber, columnNumber} = manage(sourceLocation); + return { + rawModuleId, + sourceFileURL, + lineNumber, + columnNumber, + }; + }); + return locations; + } finally { + flush(); + } + } + + async getScopeInfo(type: string): Promise { + switch (type) { + case 'GLOBAL': + return { + type, + typeName: 'Global', + icon: 'data:null', + }; + case 'LOCAL': + return { + type, + typeName: 'Local', + icon: 'data:null', + }; + case 'PARAMETER': + return { + type, + typeName: 'Parameter', + icon: 'data:null', + }; + } + throw new Error(`InternalError: Invalid scope type '${type}`); + } + + async listVariablesInScope(rawLocation: Chrome.DevTools.RawLocation): Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); + try { + const variables = manage(moduleInfo.dwarfSymbolsPlugin.ListVariablesInScope( + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0)); + const error = manage(variables.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const apiVariables = mapVector(manage(variables.variable), variable => { + const {scope, name, type} = manage(variable); + return {scope: moduleInfo.stringifyScope(scope), name, type, nestedName: name.split('::')}; + }); + return apiVariables; + } finally { + flush(); + } + } + + async getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): + Promise<{frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]}> { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); + try { + const functionInfo = + manage(moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset)); + const error = manage(functionInfo.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const apiFunctionInfos = mapVector(manage(functionInfo.functionNames), functionName => { + return {name: functionName}; + }); + let apiMissingSymbolFiles = mapVector(manage(functionInfo.missingSymbolFiles), x => x); + if (apiMissingSymbolFiles.length && this.resourceLoader.possiblyMissingSymbols) { + apiMissingSymbolFiles = apiMissingSymbolFiles.concat(this.resourceLoader.possiblyMissingSymbols); + } + + return { + frames: apiFunctionInfos, + missingSymbolFiles: apiMissingSymbolFiles.map(x => new URL(x, moduleInfo.symbolsUrl).href) + }; + } finally { + flush(); + } + } + + async getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): + Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); + try { + const rawLocations = manage( + moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); + const error = manage(rawLocations.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { + const {rawModuleId, startOffset, endOffset} = manage(rawLocation); + return {rawModuleId, startOffset, endOffset}; + }); + return locations; + } finally { + flush(); + } + } + + async getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); + try { + const rawLocations = manage( + moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); + const error = manage(rawLocations.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { + const {rawModuleId, startOffset, endOffset} = manage(rawLocation); + return {rawModuleId, startOffset, endOffset}; + }); + return locations; + } finally { + flush(); + } + } + + async getValueInfo(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): Promise<{ + typeInfos: Formatters.TypeInfo[], + root: Formatters.TypeInfo, + location?: number, + data?: number[], + displayValue?: string, + memoryAddress?: number, + }|null> { + const {manage, unmanage, flush} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(context.rawModuleId); + try { + const apiRawLocation = manage(new moduleInfo.backend.RawLocation()); + apiRawLocation.rawModuleId = context.rawModuleId; + apiRawLocation.codeOffset = context.codeOffset; + apiRawLocation.inlineFrameIndex = context.inlineFrameIndex || 0; + + const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); + const proxy = new Formatters.DebuggerProxy(wasm, moduleInfo.backend); + const typeInfoResult = + manage(moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, proxy)); + const error = manage(typeInfoResult.error); + if (error) { + if (error.code === moduleInfo.backend.ErrorCode.MODULE_NOT_FOUND_ERROR) { + // Let's not throw when the module gets unloaded - that is quite common path that + // we hit when the source-scope pane still keeps asynchronously updating while we + // unload the wasm module. + return null; + } + // TODO(crbug.com/1271147) Instead of throwing, we whould create an AST error node with the message + // so that it is properly surfaced to the user. This should then make the special handling of + // MODULE_NOT_FOUND_ERROR unnecessary. + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + + const typeInfos = mapVector(manage(typeInfoResult.typeInfos), typeInfo => fromApiTypeInfo(manage(typeInfo))); + const root = fromApiTypeInfo(manage(typeInfoResult.root)); + const {location, displayValue, memoryAddress} = typeInfoResult; + const data = typeInfoResult.data ? mapVector(manage(typeInfoResult.data), n => n) : undefined; + return {typeInfos, root, location, data, displayValue, memoryAddress}; + + function fromApiTypeInfo(apiTypeInfo: SymbolsBackend.TypeInfo): Formatters.TypeInfo { + const apiMembers = manage(apiTypeInfo.members); + const members = mapVector(apiMembers, fieldInfo => mapFieldInfo(manage(fieldInfo))); + const apiEnumerators = manage(apiTypeInfo.enumerators); + const enumerators = mapVector(apiEnumerators, enumerator => mapEnumerator(manage(enumerator))); + unmanage(apiEnumerators); + const typeNames = mapVector(manage(apiTypeInfo.typeNames), e => e); + unmanage(apiMembers); + const {typeId, size, arraySize, alignment, canExpand, isPointer, hasValue} = apiTypeInfo; + const formatter = Formatters.CustomFormatters.get({ + typeNames, + typeId, + size, + alignment, + isPointer, + canExpand, + arraySize: arraySize ?? 0, + hasValue, + members, + enumerators, + }); + return { + typeNames, + isPointer, + typeId, + size, + alignment, + canExpand: canExpand && !formatter, + arraySize: arraySize ?? 0, + hasValue: hasValue || Boolean(formatter), + members, + enumerators, + }; + } + } finally { + flush(); + } + } + + async getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { + const {flush, manage} = createEmbindPool(); + const moduleInfo = await this.getModuleInfo(rawModuleId); + const sourceFile = moduleInfo.urlToFileName.get(sourceFileURL); + if (!sourceFile) { + throw new Error(`InternalError: Unknown URL ${sourceFileURL}`); + } + + try { + const mappedLines = manage(moduleInfo.dwarfSymbolsPlugin.GetMappedLines(rawModuleId, sourceFile)); + const error = manage(mappedLines.error); + if (error) { + throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); + } + const lines = mapVector(manage(mappedLines.MappedLines), l => l); + return lines; + } finally { + flush(); + } + } + + async evaluate(expression: string, context: SymbolsBackend.RawLocation, stopId: unknown): + Promise { + const valueInfo = await this.getValueInfo(expression, context, stopId); + if (!valueInfo) { + return null; + } + + const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); + const cxxObject = await Formatters.CXXValue.create(this.lazyObjects, wasm, wasm.view, valueInfo); + if (!cxxObject) { + return { + type: 'undefined' as Chrome.DevTools.RemoteObjectType, + hasChildren: false, + description: '', + }; + } + return await cxxObject.asRemoteObject(); + } + + async getProperties(objectId: Chrome.DevTools.RemoteObjectId): Promise { + const remoteObject = this.lazyObjects.get(objectId); + if (!remoteObject) { + return []; + } + + const properties = await remoteObject.getProperties(); + const descriptors = []; + for (const {name, property} of properties) { + descriptors.push({name, value: await property.asRemoteObject()}); + } + return descriptors; + } + + async releaseObject(objectId: Chrome.DevTools.RemoteObjectId): Promise { + this.lazyObjects.release(objectId); + } +} + +export async function createPlugin( + hostInterface: HostInterface, resourceLoader: ResourceLoader, + moduleConfigurations: ModuleConfigurations = DEFAULT_MODULE_CONFIGURATIONS, + logPluginApiCalls = false): Promise { + const plugin = new DWARFLanguageExtensionPlugin(moduleConfigurations, resourceLoader, hostInterface); + if (logPluginApiCalls) { + const pluginLoggingProxy = { + get: function(target: DWARFLanguageExtensionPlugin, key: Key): + DWARFLanguageExtensionPlugin[Key] { + if (typeof target[key] === 'function') { + return function(): unknown { + const args = [...arguments]; + const jsonArgs = args.map(x => { + try { + return JSON.stringify(x); + } catch { + return x.toString(); + } + }) + .join(', '); + // eslint-disable-next-line no-console + console.info(`${key}(${jsonArgs})`); + // @ts-expect-error TypeScript does not play well with `arguments` + return (target[key] as (...args: any[]) => void).apply(target, arguments); + } as unknown as DWARFLanguageExtensionPlugin[Key]; + } + return Reflect.get(target, key); + }, + }; + + return new Proxy(plugin, pluginLoggingProxy); + } + return plugin; +} diff --git a/extensions/cxx_debugging/src/DevToolsPluginWorker.ts b/extensions/cxx_debugging/src/DevToolsPluginWorker.ts new file mode 100644 index 0000000..92511ba --- /dev/null +++ b/extensions/cxx_debugging/src/DevToolsPluginWorker.ts @@ -0,0 +1,120 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; + +import {createPlugin, type ResourceLoader} from './DWARFSymbols.js'; +import type {ModuleConfigurations} from './ModuleConfiguration.js'; +import {deserializeWasmMemory, deserializeWasmValue, kMaxWasmValueSize, type WasmValue} from './WasmTypes.js'; +import {type Channel, type HostInterface, SynchronousIOMessage, type WorkerInterface, WorkerRPC} from './WorkerRPC.js'; + +class SynchronousLinearMemoryMessage extends SynchronousIOMessage { + deserialize(length: number): ArrayBuffer { + if (length !== this.buffer.byteLength) { + throw new Error('Expected length to match the internal buffer size'); + } + return deserializeWasmMemory(this.buffer); + } +} + +class SynchronousWasmValueMessage extends SynchronousIOMessage { + deserialize(type: number): WasmValue { + return deserializeWasmValue(this.buffer, type); + } +} + +export class RPCInterface implements WorkerInterface, HostInterface { + private readonly rpc: WorkerRPC; + #plugin?: Chrome.DevTools.LanguageExtensionPlugin; + private readonly resourceLoader: ResourceLoader; + + get plugin(): Chrome.DevTools.LanguageExtensionPlugin { + if (!this.#plugin) { + throw new Error('Worker is not yet initialized'); + } + return this.#plugin; + } + + constructor(port: Channel, resourceLoader: ResourceLoader) { + this.rpc = new WorkerRPC(port, this); + this.resourceLoader = resourceLoader; + } + + getWasmLinearMemory(offset: number, length: number, stopId: unknown): ArrayBuffer { + return this.rpc.sendMessageSync( + new SynchronousLinearMemoryMessage(length), 'getWasmLinearMemory', offset, length, stopId); + } + getWasmLocal(local: number, stopId: unknown): WasmValue { + return this.rpc.sendMessageSync(new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmLocal', local, stopId); + } + getWasmGlobal(global: number, stopId: unknown): WasmValue { + return this.rpc.sendMessageSync( + new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmGlobal', global, stopId); + } + getWasmOp(op: number, stopId: unknown): WasmValue { + return this.rpc.sendMessageSync(new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmOp', op, stopId); + } + reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): + Promise { + return this.rpc.sendMessage('reportResourceLoad', resourceUrl, status); + } + + evaluate(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): + Promise { + if (this.plugin.evaluate) { + return this.plugin.evaluate(expression, context, stopId); + } + return Promise.resolve(null); + } + getProperties(objectId: string): Promise { + if (this.plugin.getProperties) { + return this.plugin.getProperties(objectId); + } + return Promise.resolve([]); + } + releaseObject(objectId: string): Promise { + if (this.plugin.releaseObject) { + return this.plugin.releaseObject(objectId); + } + return Promise.resolve(); + } + + addRawModule(rawModuleId: string, symbolsURL: string|undefined, rawModule: Chrome.DevTools.RawModule): + Promise { + return this.plugin.addRawModule(rawModuleId, symbolsURL, rawModule); + } + sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): + Promise { + return this.plugin.sourceLocationToRawLocation(sourceLocation); + } + rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): Promise { + return this.plugin.rawLocationToSourceLocation(rawLocation); + } + getScopeInfo(type: string): Promise { + return this.plugin.getScopeInfo(type); + } + listVariablesInScope(rawLocation: Chrome.DevTools.RawLocation): Promise { + return this.plugin.listVariablesInScope(rawLocation); + } + removeRawModule(rawModuleId: string): Promise { + return this.plugin.removeRawModule(rawModuleId); + } + getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): + Promise<{frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]}| + {frames: Chrome.DevTools.FunctionInfo[]}|{missingSymbolFiles: string[]}> { + return this.plugin.getFunctionInfo(rawLocation); + } + getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { + return this.plugin.getInlinedFunctionRanges(rawLocation); + } + getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { + return this.plugin.getInlinedCalleesRanges(rawLocation); + } + getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { + return this.plugin.getMappedLines(rawModuleId, sourceFileURL); + } + async hello(moduleConfigurations: ModuleConfigurations, logPluginApiCalls: boolean): Promise { + this.#plugin = await createPlugin(this, this.resourceLoader, moduleConfigurations, logPluginApiCalls); + } +} diff --git a/extensions/cxx_debugging/src/Formatters.ts b/extensions/cxx_debugging/src/Formatters.ts new file mode 100644 index 0000000..9ce1d87 --- /dev/null +++ b/extensions/cxx_debugging/src/Formatters.ts @@ -0,0 +1,290 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { + CustomFormatters, + type LazyObject, + PrimitiveLazyObject, + type TypeInfo, + type Value, + type WasmInterface, +} from './CustomFormatters.js'; +import type {ForeignObject} from './WasmTypes.js'; + +/* + * Numbers + */ +CustomFormatters.addFormatter({types: ['bool'], format: (wasm, value) => value.asUint8() > 0}); +CustomFormatters.addFormatter({types: ['uint16_t'], format: (wasm, value) => value.asUint16()}); +CustomFormatters.addFormatter({types: ['uint32_t'], format: (wasm, value) => value.asUint32()}); +CustomFormatters.addFormatter({types: ['uint64_t'], format: (wasm, value) => value.asUint64()}); + +CustomFormatters.addFormatter({types: ['int16_t'], format: (wasm, value) => value.asInt16()}); +CustomFormatters.addFormatter({types: ['int32_t'], format: (wasm, value) => value.asInt32()}); +CustomFormatters.addFormatter({types: ['int64_t'], format: (wasm, value) => value.asInt64()}); + +CustomFormatters.addFormatter({types: ['float'], format: (wasm, value) => value.asFloat32()}); +CustomFormatters.addFormatter({types: ['double'], format: (wasm, value) => value.asFloat64()}); + +export const enum Constants { + MAX_STRING_LEN = (1 << 28) - 16, // This is the maximum string len for 32bit taken from V8 + PAGE_SIZE = 1 << 12, // Block size used for formatting strings when searching for the null terminator + SAFE_HEAP_START = 1 << 10, +} +export function formatVoid(): () => LazyObject { + return () => new PrimitiveLazyObject('undefined', undefined, ''); +} + +CustomFormatters.addFormatter({types: ['void'], format: formatVoid}); + +CustomFormatters.addFormatter({types: ['uint8_t', 'int8_t'], format: formatChar}); + +export function formatChar(wasm: WasmInterface, value: Value): string { + const char = value.typeNames.includes('int8_t') ? Math.abs(value.asInt8()) : value.asUint8(); + switch (char) { + case 0x0: + return '\'\\0\''; + case 0x7: + return '\'\\a\''; + case 0x8: + return '\'\\b\''; + case 0x9: + return '\'\\t\''; + case 0xA: + return '\'\\n\''; + case 0xB: + return '\'\\v\''; + case 0xC: + return '\'\\f\''; + case 0xD: + return '\'\\r\''; + } + if (char < 0x20 || char > 0x7e) { + return `'\\x${char.toString(16).padStart(2, '0')}'`; + } + return `'${String.fromCharCode(value.asInt8())}'`; +} + +CustomFormatters.addFormatter({ + types: ['wchar_t', 'char32_t', 'char16_t'], + format: (wasm, value) => { + const codepoint = value.size === 2 ? value.asUint16() : value.asUint32(); + try { + return String.fromCodePoint(codepoint); + } catch { + return `U+${codepoint.toString(16).padStart(value.size * 2, '0')}`; + } + }, +}); + +/* + * STL + */ +function formatLibCXXString( + wasm: WasmInterface, value: Value, charType: T, + decode: (chars: InstanceType) => string): {size: number, string: string} { + const shortString = value.$('__r_.__value_..__s'); + const size = shortString.getMembers().includes('') ? shortString.$('.__size_').asUint8() : + shortString.$('__size_').asUint8(); + const isLong = 0 < (size & 0x80); + const charSize = charType.BYTES_PER_ELEMENT; + if (isLong) { + const longString = value.$('__r_.__value_..__l'); + const data = longString.$('__data_').asUint32(); + const stringSize = longString.$('__size_').asUint32(); + + const copyLen = Math.min(stringSize * charSize, Constants.MAX_STRING_LEN); + const bytes = wasm.readMemory(data, copyLen); + const text = new charType(bytes.buffer, bytes.byteOffset, stringSize) as InstanceType; + return {size: stringSize, string: decode(text)}; + } + + const bytes = shortString.$('__data_').asDataView(0, size * charSize); + const text = new charType(bytes.buffer, bytes.byteOffset, size) as InstanceType; + return {size, string: decode(text)}; +} + +export function formatLibCXX8String(wasm: WasmInterface, value: Value): {size: number, string: string} { + return formatLibCXXString(wasm, value, Uint8Array, str => new TextDecoder().decode(str)); +} + +export function formatLibCXX16String(wasm: WasmInterface, value: Value): {size: number, string: string} { + return formatLibCXXString(wasm, value, Uint16Array, str => new TextDecoder('utf-16le').decode(str)); +} + +export function formatLibCXX32String(wasm: WasmInterface, value: Value): {size: number, string: string} { + // emscripten's wchar is 4 byte + return formatLibCXXString( + wasm, value, Uint32Array, str => Array.from(str).map(v => String.fromCodePoint(v)).join('')); +} + +CustomFormatters.addFormatter({ + types: [ + 'std::__2::string', + 'std::__2::basic_string, std::__2::allocator >', + 'std::__2::u8string', + 'std::__2::basic_string, std::__2::allocator >', + ], + format: formatLibCXX8String, +}); + +CustomFormatters.addFormatter({ + types: [ + 'std::__2::u16string', + 'std::__2::basic_string, std::__2::allocator >', + ], + format: formatLibCXX16String, +}); + +CustomFormatters.addFormatter({ + types: [ + 'std::__2::wstring', + 'std::__2::basic_string, std::__2::allocator >', + 'std::__2::u32string', + 'std::__2::basic_string, std::__2::allocator >', + ], + format: formatLibCXX32String, +}); + +type CharArrayConstructor = Uint8ArrayConstructor|Uint16ArrayConstructor|Uint32ArrayConstructor; + +function formatRawString( + wasm: WasmInterface, value: Value, charType: T, decode: (chars: InstanceType) => string): string|{ + [key: string]: Value|null, +} +{ + const address = value.asUint32(); + if (address < Constants.SAFE_HEAP_START) { + return formatPointerOrReference(wasm, value); + } + const charSize = charType.BYTES_PER_ELEMENT; + const slices: DataView[] = []; + const deref = value.$('*'); + for (let bufferSize = 0; bufferSize < Constants.MAX_STRING_LEN; bufferSize += Constants.PAGE_SIZE) { + // Copy PAGE_SIZE bytes + const buffer = deref.asDataView(bufferSize, Constants.PAGE_SIZE); + // Convert to charType + const substr = new charType(buffer.buffer, buffer.byteOffset, buffer.byteLength / charSize); + const strlen = substr.indexOf(0); + if (strlen >= 0) { + // buffer size is in bytes, strlen in characters + const str = new charType(bufferSize / charSize + strlen) as InstanceType; + for (let i = 0; i < slices.length; ++i) { + str.set( + // @ts-expect-error TypeScript can't find the deduce the intersection type correctly + new charType(slices[i].buffer, slices[i].byteOffset, slices[i].byteLength / charSize), + i * Constants.PAGE_SIZE / charSize); + } + str.set(substr.subarray(0, strlen), bufferSize / charSize); + return decode(str); + } + slices.push(buffer); + } + return formatPointerOrReference(wasm, value); +} + +export function formatCString(wasm: WasmInterface, value: Value): string|{ + [key: string]: Value|null, +} +{ + return formatRawString(wasm, value, Uint8Array, str => new TextDecoder().decode(str)); +} + +export function formatU16CString(wasm: WasmInterface, value: Value): string|{ + [key: string]: Value|null, +} +{ + return formatRawString(wasm, value, Uint16Array, str => new TextDecoder('utf-16le').decode(str)); +} + +export function formatCWString(wasm: WasmInterface, value: Value): string|{ + [key: string]: Value|null, +} +{ + // emscripten's wchar is 4 byte + return formatRawString(wasm, value, Uint32Array, str => Array.from(str).map(v => String.fromCodePoint(v)).join('')); +} + +// Register with higher precedence than the generic pointer handler. +CustomFormatters.addFormatter({types: ['char *', 'char8_t *'], format: formatCString}); +CustomFormatters.addFormatter({types: ['char16_t *'], format: formatU16CString}); +CustomFormatters.addFormatter({types: ['wchar_t *', 'char32_t *'], format: formatCWString}); + +export function formatVector(wasm: WasmInterface, value: Value): Value[] { + const begin = value.$('__begin_'); + const end = value.$('__end_'); + const size = (end.asUint32() - begin.asUint32()) / begin.$('*').size; + const elements = []; + for (let i = 0; i < size; ++i) { + elements.push(begin.$(i)); + } + return elements; +} + +function reMatch(...exprs: RegExp[]): (type: TypeInfo) => boolean { + return (type: TypeInfo) => { + for (const expr of exprs) { + for (const name of type.typeNames) { + if (expr.exec(name)) { + return true; + } + } + } + + for (const expr of exprs) { + for (const name of type.typeNames) { + if (name.startsWith('const ')) { + if (expr.exec(name.substring(6))) { + return true; + } + } + } + } + return false; + }; +} + +CustomFormatters.addFormatter({types: reMatch(/^std::vector<.+>$/), format: formatVector}); + +export function formatPointerOrReference(wasm: WasmInterface, value: Value): {[key: string]: Value|null} { + const address = value.asUint32(); + if (address === 0) { + return {'0x0': null}; + } + return {[`0x${address.toString(16)}`]: value.$('*')}; +} +CustomFormatters.addFormatter({types: type => type.isPointer, format: formatPointerOrReference}); + +export function formatDynamicArray(wasm: WasmInterface, value: Value): {[key: string]: Value|null} { + return {[`0x${value.location.toString(16)}`]: value.$(0)}; +} +CustomFormatters.addFormatter({types: reMatch(/^.+\[\]$/), format: formatDynamicArray}); + +export function formatUInt128(wasm: WasmInterface, value: Value): bigint { + const view = value.asDataView(); + return (view.getBigUint64(8, true) << BigInt(64)) + (view.getBigUint64(0, true)); +} +CustomFormatters.addFormatter({types: ['unsigned __int128'], format: formatUInt128}); + +export function formatInt128(wasm: WasmInterface, value: Value): bigint { + const view = value.asDataView(); + return (view.getBigInt64(8, true) << BigInt(64)) | (view.getBigUint64(0, true)); +} +CustomFormatters.addFormatter({types: ['__int128'], format: formatInt128}); + +export function formatExternRef(wasm: WasmInterface, value: Value): () => LazyObject { + const obj = { + async getProperties(): Promise> { + return []; + }, + async asRemoteObject(): Promise { + const encodedValue = value.asUint64(); + const ValueClasses: ['global', 'local', 'operand'] = ['global', 'local', 'operand']; + const valueClass = ValueClasses[Number(encodedValue >> 32n)]; + return {type: 'reftype', valueClass, index: Number(BigInt.asUintN(32, encodedValue))}; + } + }; + return () => obj; +} +CustomFormatters.addFormatter({types: ['__externref_t', 'externref_t'], format: formatExternRef}); diff --git a/extensions/cxx_debugging/src/MEMFSResourceLoader.ts b/extensions/cxx_debugging/src/MEMFSResourceLoader.ts new file mode 100644 index 0000000..d103425 --- /dev/null +++ b/extensions/cxx_debugging/src/MEMFSResourceLoader.ts @@ -0,0 +1,102 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; + +import type * as DWARFSymbols from './DWARFSymbols.js'; +import type {HostInterface} from './WorkerRPC.js'; + +export class ResourceLoader implements DWARFSymbols.ResourceLoader { + protected async fetchSymbolsData(rawModule: DWARFSymbols.RawModule, url: URL, hostInterface: HostInterface): + Promise<{symbolsData: ArrayBuffer, symbolsDwpData?: ArrayBuffer}> { + if (rawModule.code) { + return {symbolsData: rawModule.code, symbolsDwpData: rawModule.dwp}; + } + const symbolsResponse = await fetch(url.href, {mode: 'no-cors'}); + if (symbolsResponse.ok) { + let symbolsDwpResponse = undefined; + let symbolsDwpError; + const dwpUrl = `${url.href}.dwp`; + try { + symbolsDwpResponse = await fetch(dwpUrl, {mode: 'no-cors'}); + } catch (e) { + symbolsDwpError = (e as Error).message; + // Unclear if this ever happens; usually if the file isn't there we + // get a 404 response. + console.error(symbolsDwpError); + } + if (!(symbolsDwpResponse?.ok)) { + // Often this won't exist, but remember the missing file because if + // we can't find symbol information later it is likely because this + // file was missing. + this.possiblyMissingSymbols = [`${url.pathname}.dwp`]; + if (symbolsDwpResponse) { + symbolsDwpError = symbolsDwpResponse?.statusText || `status code ${symbolsDwpResponse.status}`; + } + } + const [symbolsData, symbolsDwpData] = await Promise.all([ + symbolsResponse.arrayBuffer(), + symbolsDwpResponse?.ok ? symbolsDwpResponse.arrayBuffer() : undefined, + ]); + void hostInterface.reportResourceLoad(url.href, {success: true, size: symbolsData.byteLength}); + if (symbolsDwpData) { + void hostInterface.reportResourceLoad(dwpUrl, {success: true, size: symbolsDwpData.byteLength}); + } else { + void hostInterface.reportResourceLoad( + dwpUrl, {success: false, errorMessage: `Failed to fetch dwp file: ${symbolsDwpError}`}); + } + return {symbolsData, symbolsDwpData}; + } + const statusText = symbolsResponse.statusText || `status code ${symbolsResponse.status}`; + if (rawModule.url !== url.href) { + const errorMessage = `NotFoundError: Unable to load debug symbols from '${url}' for the WebAssembly module '${ + rawModule.url}' (${statusText}), double-check the parameter to -gseparate-dwarf in your Emscripten link step`; + void hostInterface.reportResourceLoad(url.href, {success: false, errorMessage}); + throw new Error(errorMessage); + } + const errorMessage = `NotFoundError: Unable to load debug symbols from '${url}' (${statusText})`; + void hostInterface.reportResourceLoad(url.href, {success: false, errorMessage}); + throw new Error(errorMessage); + } + + protected getModuleFileName(rawModuleId: string): string { + return `${self.btoa(rawModuleId)}.wasm`.replace(/\//g, '_'); + } + + async loadSymbols( + rawModuleId: string, rawModule: Chrome.DevTools.RawModule, symbolsURL: URL, fileSystem: typeof FS, + hostInterface: HostInterface): Promise<{symbolsFileName: string, symbolsDwpFileName: string|undefined}> { + const {symbolsData, symbolsDwpData} = await this.fetchSymbolsData(rawModule, symbolsURL, hostInterface); + const symbolsFileName = this.getModuleFileName(rawModuleId); + const symbolsDwpFileName = symbolsDwpData && `${symbolsFileName}.dwp`; + + // This file is sometimes preserved on reload, causing problems. + try { + fileSystem.unlink('/' + symbolsFileName); + } catch { + } + + fileSystem.createDataFile( + '/', symbolsFileName, new Uint8Array(symbolsData), true /* canRead */, false /* canWrite */, true /* canOwn */); + if (symbolsDwpData && symbolsDwpFileName) { + fileSystem.createDataFile( + '/', symbolsDwpFileName, new Uint8Array(symbolsDwpData), true /* canRead */, false /* canWrite */, + true /* canOwn */); + } + + return {symbolsFileName, symbolsDwpFileName}; + } + + createSymbolsBackendModulePromise(): Promise { + const url = new URL('SymbolsBackend.wasm', import.meta.url); + return fetch(url.href, {credentials: 'same-origin'}).then(response => { + if (!response.ok) { + throw new Error(response.statusText); + } + return WebAssembly.compileStreaming(response); + }); + } + + possiblyMissingSymbols?: string[]; +} diff --git a/extensions/cxx_debugging/src/ModuleConfiguration.ts b/extensions/cxx_debugging/src/ModuleConfiguration.ts new file mode 100644 index 0000000..f4f0e8c --- /dev/null +++ b/extensions/cxx_debugging/src/ModuleConfiguration.ts @@ -0,0 +1,117 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {globMatch} from './GlobMatch.js'; + +/** + * A path substitution specifies a string prefix pattern to be + * replaced with a new string. This is the pendant of the + * `set substitute-path old new` feature that is found in GDB + * and `settings set target.source-map old new` feature that + * is found in LLDB. + */ +export interface PathSubstitution { + readonly from: string; readonly to: string; +} + +/** + * List of {@type PathSubstitution}s. + */ +export type PathSubstitutions = readonly PathSubstitution[]; + +/** + * Resolve a source path (as stored in DWARF debugging information) to an absolute URL. + * + * Note that we treat "." specially as a pattern, since LLDB normalizes paths before + * returning them from the DWARF parser. Our logic replicates the logic found in the + * LLDB frontend in `PathMappingList::RemapPath()` inside `Target/PathMappingList.cpp` + * (http://cs/github/llvm/llvm-project/lldb/source/Target/PathMappingList.cpp?l=157-185). + * + * @param pathSubstitutions possible substitutions to apply to the {@param sourcePath}, applies the first match. + * @param sourcePath the source path as found in the debugging information. + * @param baseURL the URL of the WebAssembly module, which is used to resolve relative source paths. + * @return an absolute `file:`-URI or a URL relative to the {@param baseURL}. + */ +export function resolveSourcePathToURL(pathSubstitutions: PathSubstitutions, sourcePath: string, baseURL: URL): URL { + // Normalize '\' to '/' in sourcePath first. + let resolvedSourcePath = sourcePath.replace(/\\/g, '/'); + + // Apply source path substitutions first. + for (const {from, to} of pathSubstitutions) { + if (resolvedSourcePath.startsWith(from)) { + resolvedSourcePath = to + resolvedSourcePath.slice(from.length); + break; + } + + // Relative paths won't have a leading "./" in them unless "." is the only + // thing in the relative path so we need to work around "." carefully. + if (from === '.') { + // We need to figure whether sourcePath can be considered a relative path, + // ruling out absolute POSIX and Windows paths, as well as file:, http: and + // https: URLs. + if (!resolvedSourcePath.startsWith('/') && !/^([A-Z]|file|https?):/i.test(resolvedSourcePath)) { + resolvedSourcePath = `${to}/${resolvedSourcePath}`; + break; + } + } + } + + if (resolvedSourcePath.startsWith('/')) { + if (resolvedSourcePath.startsWith('//')) { + return new URL(`file:${resolvedSourcePath}`); + } + return new URL(`file://${resolvedSourcePath}`); + } + if (/^[A-Z]:/i.test(resolvedSourcePath)) { + return new URL(`file:/${resolvedSourcePath}`); + } + return new URL(resolvedSourcePath, baseURL.href); +} + +/** + * Configuration for locating source files for a given WebAssembly module. + * If the name is `undefined`, the configuration is the default configuration, + * which is chosen if there's no named configuration matching the basename of + * the WebAssembly module file. + * The name can be a wildcard pattern, and {@see globMatch} will be used to + * match the name against the URL of the WebAssembly module file. + */ +export interface ModuleConfiguration { + readonly name?: string; readonly pathSubstitutions: PathSubstitutions; +} + +/** + * List of {@type ModuleConfiguration}s. These lists are intended to have + * a default configuration, whose name field is `undefined`, which is chosen + * when no matching named configuration is found. + */ +export type ModuleConfigurations = readonly ModuleConfiguration[]; + +/** + * Locate the configuration for a given `something.wasm` module file name. + * + * @param moduleConfigurations list of module configurations to scan. + * @param moduleName the URL of the module to lookup. + * @return the matching module configuration or the default fallback. + */ +export function findModuleConfiguration( + moduleConfigurations: ModuleConfigurations, moduleURL: URL): ModuleConfiguration { + let defaultModuleConfiguration: ModuleConfiguration = {pathSubstitutions: []}; + for (const moduleConfiguration of moduleConfigurations) { + // The idea here is that module configurations will have at most + // one default configuration, so picking the last here is fine. + if (moduleConfiguration.name === undefined) { + defaultModuleConfiguration = moduleConfiguration; + continue; + } + + // Perform wildcard pattern matching on the full URL. + if (globMatch(moduleConfiguration.name, moduleURL.href)) { + return moduleConfiguration; + } + } + return defaultModuleConfiguration; +} + +export const DEFAULT_MODULE_CONFIGURATIONS: ModuleConfigurations = [{pathSubstitutions: []}]; diff --git a/extensions/cxx_debugging/src/SymbolsBackend.cc b/extensions/cxx_debugging/src/SymbolsBackend.cc new file mode 100644 index 0000000..cb0763d --- /dev/null +++ b/extensions/cxx_debugging/src/SymbolsBackend.cc @@ -0,0 +1,262 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// clang-format off +#include +#include + +#include +#include +#include "ApiContext.h" +#include "WasmVendorPlugins.h" + +#include "Plugins/Language/CPlusPlus/CPlusPlusLanguage.h" +#include "Plugins/ObjectFile/wasm/ObjectFileWasm.h" +#include "Plugins/ScriptInterpreter/None/ScriptInterpreterNone.h" +#include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h" +#include "Plugins/SymbolVendor/wasm/SymbolVendorWasm.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/linux/HostInfoLinux.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Base64.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/raw_ostream.h" + +namespace { + +struct DefaultPluginsContext + : symbols_backend::PluginRegistryContext< + lldb_private::HostInfoPosix, + symbols_backend::WasmPlatform, + lldb_private::ScriptInterpreterNone, + lldb_private::FileSystem, + lldb_private::CPlusPlusLanguage, + lldb_private::TypeSystemClang, + lldb_private::wasm::ObjectFileWasm, + lldb_private::wasm::SymbolVendorWasm, + symbols_backend::WasmProcess, + symbols_backend::SymbolFileWasmDWARF> { + DefaultPluginsContext() : PluginRegistryContext() { + lldb_private::Debugger::Initialize(nullptr); + } + ~DefaultPluginsContext() { lldb_private::Debugger::Terminate(); } +}; + +DefaultPluginsContext& GetGlobalContext() { + static DefaultPluginsContext global_context; + return global_context; +} + +template +std::function OptionalSetter( + ClassT& (ClassT::*setter)(llvm::Optional)) { + return [setter](ClassT& cls, emscripten::val value) { + if (value == emscripten::val::undefined() || + value == emscripten::val::null()) { + (cls.*setter)(llvm::None); + } else { + (cls.*setter)(value.as()); + } + }; +} + +template +std::function OptionalGetter( + llvm::Optional (ClassT::*getter)() const) { + return [getter](const ClassT& cls) { + if (auto value = (cls.*getter)()) { + return emscripten::val(std::move(*value)); + } + return emscripten::val::undefined(); + }; +} +} // namespace + +namespace symbols_backend { +class DWARFSymbolsPlugin : public api::ApiContext { + public: + DWARFSymbolsPlugin() : context_(GetGlobalContext()) {} + + private: + DefaultPluginsContext& context_; +}; +} // namespace symbols_backend + + + + +EMSCRIPTEN_BINDINGS(DWARFSymbolsPlugin) { + emscripten::enum_("ErrorCode") + .value("INTERNAL_ERROR", symbols_backend::api::Error::Code::kInternalError) + .value("PROTOCOL_ERROR", symbols_backend::api::Error::Code::kProtocolError) + .value("MODULE_NOT_FOUND_ERROR", symbols_backend::api::Error::Code::kModuleNotFoundError) + .value("EVAL_ERROR", symbols_backend::api::Error::Code::kEvalError) + ; + + emscripten::class_("Error") + .constructor<>() + .property("code", &symbols_backend::api::Error::GetCode, &symbols_backend::api::Error::SetCode) + .property("message", &symbols_backend::api::Error::GetMessage, &symbols_backend::api::Error::SetMessage) + ; + + emscripten::class_("RawLocationRange") + .constructor<>() + .property("rawModuleId", &symbols_backend::api::RawLocationRange::GetRawModuleId, &symbols_backend::api::RawLocationRange::SetRawModuleId) + .property("startOffset", &symbols_backend::api::RawLocationRange::GetStartOffset, &symbols_backend::api::RawLocationRange::SetStartOffset) + .property("endOffset", &symbols_backend::api::RawLocationRange::GetEndOffset, &symbols_backend::api::RawLocationRange::SetEndOffset) + ; + + emscripten::class_("RawLocation") + .constructor<>() + .property("rawModuleId", &symbols_backend::api::RawLocation::GetRawModuleId, &symbols_backend::api::RawLocation::SetRawModuleId) + .property("codeOffset", &symbols_backend::api::RawLocation::GetCodeOffset, &symbols_backend::api::RawLocation::SetCodeOffset) + .property("inlineFrameIndex", &symbols_backend::api::RawLocation::GetInlineFrameIndex, &symbols_backend::api::RawLocation::SetInlineFrameIndex) + ; + + emscripten::class_("SourceLocation") + .constructor<>() + .property("rawModuleId", &symbols_backend::api::SourceLocation::GetRawModuleId, &symbols_backend::api::SourceLocation::SetRawModuleId) + .property("sourceFile", &symbols_backend::api::SourceLocation::GetSourceFile, &symbols_backend::api::SourceLocation::SetSourceFile) + .property("lineNumber", &symbols_backend::api::SourceLocation::GetLineNumber, &symbols_backend::api::SourceLocation::SetLineNumber) + .property("columnNumber", &symbols_backend::api::SourceLocation::GetColumnNumber, &symbols_backend::api::SourceLocation::SetColumnNumber) + ; + emscripten::enum_("VariableScope") + .value("LOCAL", symbols_backend::api::Variable::Scope::kLocal) + .value("PARAMETER", symbols_backend::api::Variable::Scope::kParameter) + .value("GLOBAL", symbols_backend::api::Variable::Scope::kGlobal) + ; + + emscripten::class_("Variable") + .constructor<>() + .property("scope", &symbols_backend::api::Variable::GetScope, &symbols_backend::api::Variable::SetScope) + .property("name", &symbols_backend::api::Variable::GetName, &symbols_backend::api::Variable::SetName) + .property("type", &symbols_backend::api::Variable::GetType, &symbols_backend::api::Variable::SetType) + .property("typedefs", &symbols_backend::api::Variable::GetTypedefs, &symbols_backend::api::Variable::SetTypedefs) + ; + + emscripten::class_("FieldInfo") + .constructor<>() + .property("name", OptionalGetter(&symbols_backend::api::FieldInfo::GetName), OptionalSetter(&symbols_backend::api::FieldInfo::SetName)) + .property("offset", &symbols_backend::api::FieldInfo::GetOffset, &symbols_backend::api::FieldInfo::SetOffset) + .property("typeId", &symbols_backend::api::FieldInfo::GetTypeId, &symbols_backend::api::FieldInfo::SetTypeId) + ; + + emscripten::class_("Enumerator") + .constructor<>() + .property("name", &symbols_backend::api::Enumerator::GetName, &symbols_backend::api::Enumerator::SetName) + .property("value", &symbols_backend::api::Enumerator::GetValue, &symbols_backend::api::Enumerator::SetValue) + .property("typeId", &symbols_backend::api::Enumerator::GetTypeId, &symbols_backend::api::Enumerator::SetTypeId) + ; + + emscripten::class_("TypeInfo") + .constructor<>() + .property("typeNames", &symbols_backend::api::TypeInfo::GetTypeNames, &symbols_backend::api::TypeInfo::SetTypeNames) + .property("typeId", &symbols_backend::api::TypeInfo::GetTypeId, &symbols_backend::api::TypeInfo::SetTypeId) + .property("alignment", &symbols_backend::api::TypeInfo::GetAlignment, &symbols_backend::api::TypeInfo::SetAlignment) + .property("size", &symbols_backend::api::TypeInfo::GetSize, &symbols_backend::api::TypeInfo::SetSize) + .property("canExpand", &symbols_backend::api::TypeInfo::GetCanExpand, &symbols_backend::api::TypeInfo::SetCanExpand) + .property("hasValue", &symbols_backend::api::TypeInfo::GetHasValue, &symbols_backend::api::TypeInfo::SetHasValue) + .property("arraySize", OptionalGetter(&symbols_backend::api::TypeInfo::GetArraySize), OptionalSetter(&symbols_backend::api::TypeInfo::SetArraySize)) + .property("isPointer", &symbols_backend::api::TypeInfo::GetIsPointer, &symbols_backend::api::TypeInfo::SetIsPointer) + .property("members", &symbols_backend::api::TypeInfo::GetMembers, &symbols_backend::api::TypeInfo::SetMembers) + .property("enumerators", &symbols_backend::api::TypeInfo::GetEnumerators, &symbols_backend::api::TypeInfo::SetEnumerators) + ; + + emscripten::class_("AddRawModuleResponse") + .constructor<>() + .property("sources", &symbols_backend::api::AddRawModuleResponse::GetSources, &symbols_backend::api::AddRawModuleResponse::SetSources) + .property("dwos", &symbols_backend::api::AddRawModuleResponse::GetDwos, &symbols_backend::api::AddRawModuleResponse::SetDwos) + .property("error", OptionalGetter(&symbols_backend::api::AddRawModuleResponse::GetError), OptionalSetter(&symbols_backend::api::AddRawModuleResponse::SetError)) + ; + + emscripten::class_("SourceLocationToRawLocationResponse") + .constructor<>() + .property("rawLocationRanges", &symbols_backend::api::SourceLocationToRawLocationResponse::GetRawLocationRanges, &symbols_backend::api::SourceLocationToRawLocationResponse::SetRawLocationRanges) + .property("error", OptionalGetter(&symbols_backend::api::SourceLocationToRawLocationResponse::GetError), OptionalSetter(&symbols_backend::api::SourceLocationToRawLocationResponse::SetError)) + ; + + emscripten::class_("RawLocationToSourceLocationResponse") + .constructor<>() + .property("sourceLocation", &symbols_backend::api::RawLocationToSourceLocationResponse::GetSourceLocation, &symbols_backend::api::RawLocationToSourceLocationResponse::SetSourceLocation) + .property("error", OptionalGetter(&symbols_backend::api::RawLocationToSourceLocationResponse::GetError), OptionalSetter(&symbols_backend::api::RawLocationToSourceLocationResponse::SetError)) + ; + + emscripten::class_("ListVariablesInScopeResponse") + .constructor<>() + .property("variable", &symbols_backend::api::ListVariablesInScopeResponse::GetVariable, &symbols_backend::api::ListVariablesInScopeResponse::SetVariable) + .property("error", OptionalGetter(&symbols_backend::api::ListVariablesInScopeResponse::GetError), OptionalSetter(&symbols_backend::api::ListVariablesInScopeResponse::SetError)) + ; + + emscripten::class_("GetFunctionInfoResponse") + .constructor<>() + .property("functionNames", &symbols_backend::api::GetFunctionInfoResponse::GetFunctionNames, &symbols_backend::api::GetFunctionInfoResponse::SetFunctionNames) + .property("missingSymbolFiles", &symbols_backend::api::GetFunctionInfoResponse::GetMissingSymbolFiles, &symbols_backend::api::GetFunctionInfoResponse::SetMissingSymbolFiles) + .property("error", OptionalGetter(&symbols_backend::api::GetFunctionInfoResponse::GetError), OptionalSetter(&symbols_backend::api::GetFunctionInfoResponse::SetError)) + ; + + emscripten::class_("GetInlinedFunctionRangesResponse") + .constructor<>() + .property("rawLocationRanges", &symbols_backend::api::GetInlinedFunctionRangesResponse::GetRawLocationRanges, &symbols_backend::api::GetInlinedFunctionRangesResponse::SetRawLocationRanges) + .property("error", OptionalGetter(&symbols_backend::api::GetInlinedFunctionRangesResponse::GetError), OptionalSetter(&symbols_backend::api::GetInlinedFunctionRangesResponse::SetError)) + ; + + emscripten::class_("GetInlinedCalleesRangesResponse") + .constructor<>() + .property("rawLocationRanges", &symbols_backend::api::GetInlinedCalleesRangesResponse::GetRawLocationRanges, &symbols_backend::api::GetInlinedCalleesRangesResponse::SetRawLocationRanges) + .property("error", OptionalGetter(&symbols_backend::api::GetInlinedCalleesRangesResponse::GetError), OptionalSetter(&symbols_backend::api::GetInlinedCalleesRangesResponse::SetError)) + ; + + emscripten::class_("GetMappedLinesResponse") + .constructor<>() + .property("MappedLines", &symbols_backend::api::GetMappedLinesResponse::GetMappedLines, &symbols_backend::api::GetMappedLinesResponse::SetMappedLines) + .property("error", OptionalGetter(&symbols_backend::api::GetMappedLinesResponse::GetError), OptionalSetter(&symbols_backend::api::GetMappedLinesResponse::SetError)) + ; + + emscripten::class_("EvaluateExpressionResponse") + .constructor<>() + .property("typeInfos", &symbols_backend::api::EvaluateExpressionResponse::GetTypeInfos, &symbols_backend::api::EvaluateExpressionResponse::SetTypeInfos) + .property("root", &symbols_backend::api::EvaluateExpressionResponse::GetRoot, &symbols_backend::api::EvaluateExpressionResponse::SetRoot) + .property("displayValue", OptionalGetter(&symbols_backend::api::EvaluateExpressionResponse::GetDisplayValue), OptionalSetter(&symbols_backend::api::EvaluateExpressionResponse::SetDisplayValue)) + .property("location", OptionalGetter(&symbols_backend::api::EvaluateExpressionResponse::GetLocation), OptionalSetter(&symbols_backend::api::EvaluateExpressionResponse::SetLocation)) + .property("memoryAddress", OptionalGetter(&symbols_backend::api::EvaluateExpressionResponse::GetMemoryAddress), OptionalSetter(&symbols_backend::api::EvaluateExpressionResponse::SetMemoryAddress)) + .property("data", OptionalGetter(&symbols_backend::api::EvaluateExpressionResponse::GetData), OptionalSetter(&symbols_backend::api::EvaluateExpressionResponse::SetData)) + .property("error", OptionalGetter(&symbols_backend::api::EvaluateExpressionResponse::GetError), OptionalSetter(&symbols_backend::api::EvaluateExpressionResponse::SetError)) + ; + emscripten::register_vector("StringArray"); + emscripten::register_vector("RawLocationRangeArray"); + emscripten::register_vector("SourceLocationArray"); + emscripten::register_vector("VariableArray"); + emscripten::register_vector("Int32_TArray"); + emscripten::register_vector("TypeInfoArray"); + emscripten::register_vector("FieldInfoArray"); + emscripten::register_vector("EnumeratorArray"); + emscripten::register_vector("ByteArray"); + + emscripten::class_("DWARFSymbolsPluginBase") + .constructor<>() + .function("AddRawModule", + &symbols_backend::DWARFSymbolsPlugin::AddRawModule) + .function("RemoveRawModule", + &symbols_backend::DWARFSymbolsPlugin::RemoveRawModule) + .function("SourceLocationToRawLocation", + &symbols_backend::DWARFSymbolsPlugin::SourceLocationToRawLocation) + .function("RawLocationToSourceLocation", + &symbols_backend::DWARFSymbolsPlugin::RawLocationToSourceLocation) + .function("ListVariablesInScope", + &symbols_backend::DWARFSymbolsPlugin::ListVariablesInScope) + .function("GetFunctionInfo", + &symbols_backend::DWARFSymbolsPlugin::GetFunctionInfo) + .function("GetInlinedFunctionRanges", + &symbols_backend::DWARFSymbolsPlugin::GetInlinedFunctionRanges) + .function("GetInlinedCalleesRanges", + &symbols_backend::DWARFSymbolsPlugin::GetInlinedCalleesRanges) + .function("GetMappedLines", + &symbols_backend::DWARFSymbolsPlugin::GetMappedLines) + .function("EvaluateExpression", + &symbols_backend::DWARFSymbolsPlugin::EvaluateExpression) + ; + emscripten::class_>("DWARFSymbolsPlugin") + .constructor<>(); + +} diff --git a/extensions/cxx_debugging/src/SymbolsBackend.d.ts b/extensions/cxx_debugging/src/SymbolsBackend.d.ts new file mode 100644 index 0000000..1cfc747 --- /dev/null +++ b/extensions/cxx_debugging/src/SymbolsBackend.d.ts @@ -0,0 +1,179 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface EmbindObject { + new(): this; + delete(): void; +} + +export interface Vector extends EmbindObject { + size(): number; + get(index: number): T; + push_back(value: T): void; +} + +export interface ErrorCode extends EmbindObject {} + +export interface Error extends EmbindObject { + code: ErrorCode; + message: string; +} + +export interface RawLocationRange extends EmbindObject { + rawModuleId: string; + startOffset: number; + endOffset: number; +} + +export interface RawLocation extends EmbindObject { + rawModuleId: string; + codeOffset: number; + inlineFrameIndex: number; +} + +export interface SourceLocation extends EmbindObject { + rawModuleId: string; + sourceFile: string; + lineNumber: number; + columnNumber: number; +} + +export interface VariableScope extends EmbindObject {} + +export interface Variable extends EmbindObject { + scope: VariableScope; + name: string; + type: string; + typedefs: Vector; +} + +export interface FieldInfo extends EmbindObject { + name: string|undefined; + offset: number; + typeId: string; +} + +export interface Enumerator extends EmbindObject { + name: string; + value: bigint; + typeId: string; +} + +export interface TypeInfo extends EmbindObject { + typeNames: Vector; + typeId: string; + alignment: number; + size: number; + canExpand: boolean; + hasValue: boolean; + arraySize: number|undefined; + isPointer: boolean; + members: Vector; + enumerators: Vector; +} + +export interface AddRawModuleResponse extends EmbindObject { + sources: Vector; + dwos: Vector; + error: Error|undefined; +} + +export interface SourceLocationToRawLocationResponse extends EmbindObject { + rawLocationRanges: Vector; + error: Error|undefined; +} + +export interface RawLocationToSourceLocationResponse extends EmbindObject { + sourceLocation: Vector; + error: Error|undefined; +} + +export interface ListVariablesInScopeResponse extends EmbindObject { + variable: Vector; + error: Error|undefined; +} + +export interface GetFunctionInfoResponse extends EmbindObject { + functionNames: Vector; + missingSymbolFiles: Vector; + error: Error|undefined; +} + +export interface GetInlinedFunctionRangesResponse extends EmbindObject { + rawLocationRanges: Vector; + error: Error|undefined; +} + +export interface GetInlinedCalleesRangesResponse extends EmbindObject { + rawLocationRanges: Vector; + error: Error|undefined; +} + +export interface GetMappedLinesResponse extends EmbindObject { + MappedLines: Vector; + error: Error|undefined; +} + +export interface EvaluateExpressionResponse extends EmbindObject { + typeInfos: Vector; + root: TypeInfo; + displayValue: string|undefined; + location: number|undefined; + memoryAddress: number|undefined; + data: Vector|undefined; + error: Error|undefined; +} + +export interface DWARFSymbolsPlugin extends EmbindObject { + AddRawModule(rawModuleId: string, path: string): AddRawModuleResponse; + RemoveRawModule(rawModuleId: string): void; + SourceLocationToRawLocation(rawModuleId: string, sourceFileURL: string, lineNumber: number, columnNumber: number): + SourceLocationToRawLocationResponse; + RawLocationToSourceLocation(rawModuleId: string, codeOffset: number, inlineFrameIndex: number): + RawLocationToSourceLocationResponse; + ListVariablesInScope(rawModuleId: string, codeOffset: number, inlineFrameIndex: number): ListVariablesInScopeResponse; + GetFunctionInfo(rawModuleId: string, codeOffset: number): GetFunctionInfoResponse; + GetInlinedFunctionRanges(rawModuleId: string, codeOffset: number): GetInlinedFunctionRangesResponse; + GetInlinedCalleesRanges(rawModuleId: string, codeOffset: number): GetInlinedCalleesRangesResponse; + GetMappedLines(rawModuleId: string, sourceFileURL: string): GetMappedLinesResponse; + EvaluateExpression(location: RawLocation, expression: string, debugProxy: unknown): EvaluateExpressionResponse; +} + +export interface Module extends EmscriptenModule { + FS: typeof FS; + DWARFSymbolsPlugin: DWARFSymbolsPlugin; + StringArray: Vector; + RawLocationRangeArray: Vector; + SourceLocationArray: Vector; + VariableArray: Vector; + Int32_TArray: Vector; + TypeInfoArray: Vector; + FieldInfoArray: Vector; + EnumeratorArray: Vector; + ErrorCode: + {INTERNAL_ERROR: ErrorCode, PROTOCOL_ERROR: ErrorCode, MODULE_NOT_FOUND_ERROR: ErrorCode, EVAL_ERROR: ErrorCode}; + Error: Error; + RawLocationRange: RawLocationRange; + RawLocation: RawLocation; + SourceLocation: SourceLocation; + VariableScope: {LOCAL: VariableScope, PARAMETER: VariableScope, GLOBAL: VariableScope}; + Variable: Variable; + FieldInfo: FieldInfo; + Enumerator: Enumerator; + TypeInfo: TypeInfo; + AddRawModuleResponse: AddRawModuleResponse; + SourceLocationToRawLocationResponse: SourceLocationToRawLocationResponse; + RawLocationToSourceLocationResponse: RawLocationToSourceLocationResponse; + ListVariablesInScopeResponse: ListVariablesInScopeResponse; + GetFunctionInfoResponse: GetFunctionInfoResponse; + GetInlinedFunctionRangesResponse: GetInlinedFunctionRangesResponse; + GetInlinedCalleesRangesResponse: GetInlinedCalleesRangesResponse; + GetMappedLinesResponse: GetMappedLinesResponse; + EvaluateExpressionResponse: EvaluateExpressionResponse; +} + +declare let createSymbolsBackend: EmscriptenModuleFactory; +export default createSymbolsBackend; diff --git a/extensions/cxx_debugging/src/WasmTypes.ts b/extensions/cxx_debugging/src/WasmTypes.ts new file mode 100644 index 0000000..499b78d --- /dev/null +++ b/extensions/cxx_debugging/src/WasmTypes.ts @@ -0,0 +1,119 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI'; + +export type ForeignObject = Chrome.DevTools.ForeignObject; + +export type WasmValue = Chrome.DevTools.WasmValue; + +export type WasmSimdValue = string; + +export type WasmPrimitive = number|bigint|WasmSimdValue; + +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/prefer-enum-initializers */ +export const enum SerializedWasmType { + i32 = 1, + i64, + f32, + f64, + v128, + reftype +} +/* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/prefer-enum-initializers */ + +export function serializeWasmValue(value: WasmValue|ArrayBuffer, buffer: ArrayBufferLike): SerializedWasmType { + if (value instanceof ArrayBuffer) { + const data = new Uint8Array(value); + new Uint8Array(buffer).set(data); + return data.byteLength || -1; + } + + const view = new DataView(buffer); + switch (value.type) { + case 'i32': { + view.setInt32(0, value.value, true); + return SerializedWasmType.i32; + } + case 'i64': { + view.setBigInt64(0, value.value, true); + return SerializedWasmType.i64; + } + case 'f32': { + view.setFloat32(0, value.value, true); + return SerializedWasmType.f32; + } + case 'f64': { + view.setFloat64(0, value.value, true); + return SerializedWasmType.f64; + } + case 'v128': { + const [, a, b, c, d] = (value.value).split(' '); + view.setInt32(0, Number(a), true); + view.setInt32(4, Number(b), true); + view.setInt32(8, Number(c), true); + view.setInt32(12, Number(d), true); + return SerializedWasmType.v128; + } + case 'reftype': { + view.setUint8(0, ['local', 'global', 'operand'].indexOf(value.valueClass)); + view.setUint32(1, value.index, true); + return SerializedWasmType.reftype; + } + default: + throw new Error('cannot serialize non-numerical wasm type'); + } +} + +export function deserializeWasmMemory(buffer: ArrayBufferLike): ArrayBuffer { + const result = new Uint8Array(buffer.byteLength); + result.set(new Uint8Array(buffer)); + return result.buffer; +} + +export function deserializeWasmValue(buffer: ArrayBufferLike, type: SerializedWasmType): WasmValue { + const view = new DataView(buffer); + switch (type) { + case SerializedWasmType.i32: + return {type: 'i32', value: view.getInt32(0, true)}; + case SerializedWasmType.i64: + return {type: 'i64', value: view.getBigInt64(0, true)}; + case SerializedWasmType.f32: + return {type: 'f32', value: view.getFloat32(0, true)}; + case SerializedWasmType.f64: + return {type: 'f64', value: view.getFloat64(0, true)}; + case SerializedWasmType.v128: { + const a = view.getUint32(0, true); + const b = view.getUint32(4, true); + const c = view.getUint32(8, true); + const d = view.getUint32(12, true); + return { + type: 'v128', + value: `i32x4 0x${a.toString(16).padStart(8, '0')} 0x${b.toString(16).padStart(8, '0')} 0x${ + c.toString(16).padStart(8, '0')} 0x${d.toString(16).padStart(8, '0')}` + }; + } + case SerializedWasmType.reftype: { + const ValueClasses: ['local', 'global', 'operand'] = ['local', 'global', 'operand']; + const valueClass = ValueClasses[view.getUint8(0)]; + const index = view.getUint32(1, true); + return {type: 'reftype', valueClass, index}; + } + } + // @ts-expect-error + throw new Error('Invalid primitive wasm type'); +} + +export const kMaxWasmValueSize = 4 + 4 + 4 * 10; + +export type WasmFunction = (...args: WasmPrimitive[]) => WasmPrimitive; + +export type WasmExport = { + name: string, +}&({func: number}|{table: number}|{mem: number}|{global: number}); + +export type WasmImport = { + name: string, + module: string, +}&({func: number}|{table: number}|{mem: number}|{global: number}); diff --git a/extensions/cxx_debugging/src/WorkerRPC.ts b/extensions/cxx_debugging/src/WorkerRPC.ts new file mode 100644 index 0000000..4961e01 --- /dev/null +++ b/extensions/cxx_debugging/src/WorkerRPC.ts @@ -0,0 +1,159 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; + +import type {ModuleConfigurations} from './ModuleConfiguration.js'; +import {type SerializedWasmType, serializeWasmValue, type WasmValue} from './WasmTypes.js'; + +export interface WorkerInterface extends Chrome.DevTools.LanguageExtensionPlugin { + hello(moduleConfigurations: ModuleConfigurations, logPluginApiCalls: boolean): void; +} + +export interface AsyncHostInterface { + getWasmLinearMemory(offset: number, length: number, stopId: unknown): Promise; + getWasmLocal(local: number, stopId: unknown): Promise; + getWasmGlobal(global: number, stopId: unknown): Promise; + getWasmOp(op: number, stopId: unknown): Promise; + reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): + Promise; +} + +export interface HostInterface { + getWasmLinearMemory(offset: number, length: number, stopId: unknown): ArrayBuffer; + getWasmLocal(local: number, stopId: unknown): WasmValue; + getWasmGlobal(global: number, stopId: unknown): WasmValue; + getWasmOp(op: number, stopId: unknown): WasmValue; + reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): + Promise; +} + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type AllMessages> = { + [k in keyof Interface]: {method: k, params: Parameters} +}; + +type Message = { + requestId: number, +}&({request: AllMessages[keyof AllMessages]}|{ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + sync_request: { + request: AllMessages[keyof AllMessages], + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + io_buffer: {semaphore: SharedArrayBuffer, data: SharedArrayBuffer}, + }, +}); + +export interface Channel { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + postMessage(message: /* Response|Message*/ any): any; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + onmessage: ((e: MessageEvent|Response>) => any)|null; +} + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type AllResponses> = { + [k in keyof Interface]: ReturnType +}; + +type Response = { + requestId: number, +}&({error: string}|{response: AllResponses[keyof AllResponses]}); + +export abstract class SynchronousIOMessage { + readonly buffer: SharedArrayBuffer; + constructor(bufferSize: number) { + this.buffer = new SharedArrayBuffer(bufferSize); + } + + abstract deserialize(response: number): T; + + static serialize(value: ArrayBuffer|WasmValue, buffer: SharedArrayBuffer): SerializedWasmType { + return serializeWasmValue(value, buffer); + } +} + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export class WorkerRPC, RemoteInterface extends Record> { + private nextRequestId = 0; + private readonly channel: Channel; + private readonly localHandler: LocalInterface; + private readonly requests = new Map void, reject: (message: Error) => void}>(); + private readonly semaphore: Int32Array; + + constructor(channel: Channel, localHandler: LocalInterface) { + this.channel = channel; + this.channel.onmessage = this.onmessage.bind(this); + this.localHandler = localHandler; + this.semaphore = new Int32Array(new SharedArrayBuffer(4)); + } + + sendMessage(method: Method, ...params: Parameters): + ReturnType { + const requestId = this.nextRequestId++; + const promise = new Promise((resolve, reject) => { + this.requests.set(requestId, {resolve, reject}); + }); + this.channel.postMessage({requestId, request: {method, params}}); + return promise as ReturnType; + } + + sendMessageSync( + message: SynchronousIOMessage>, method: Method, + ...params: Parameters): ReturnType { + const requestId = this.nextRequestId++; + Atomics.store(this.semaphore, 0, 0); + this.channel.postMessage({ + requestId, + sync_request: { + request: {method, params}, + io_buffer: {semaphore: this.semaphore.buffer as SharedArrayBuffer, data: message.buffer}, + }, + }); + while (Atomics.wait(this.semaphore, 0, 0) !== 'not-equal') { + // Await the semaphore to be equal + } + const [response] = this.semaphore; + + return message.deserialize(response); + } + + private async onmessage( + event: MessageEvent|Message|Response>): Promise { + if ('request' in event.data) { + const {requestId, request} = event.data; + try { + const response = await this.localHandler[request.method](...request.params); + this.channel.postMessage({requestId, response}); + } catch (error) { + this.channel.postMessage({requestId, error: `${error}`}); + } + } else if ('sync_request' in event.data) { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const {sync_request: {request, io_buffer}} = event.data; + let signal = -1; + try { + const response = await this.localHandler[request.method](...request.params); + signal = SynchronousIOMessage.serialize(response, io_buffer.data); + } catch (error) { + throw error; + } finally { + const semaphore = new Int32Array(io_buffer.semaphore); + Atomics.store(semaphore, 0, signal); + Atomics.notify(semaphore, 0); + } + } else { + const {requestId} = event.data; + const callbacks = this.requests.get(requestId); + if (callbacks) { + const {resolve, reject} = callbacks; + if ('error' in event.data) { + reject(new Error(event.data.error)); + } else { + resolve(event.data.response); + } + } + } + } +} diff --git a/extensions/cxx_debugging/tests/CMakeLists.txt b/extensions/cxx_debugging/tests/CMakeLists.txt new file mode 100644 index 0000000..8179092 --- /dev/null +++ b/extensions/cxx_debugging/tests/CMakeLists.txt @@ -0,0 +1,146 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +add_custom_target(check-extension + COMMAND + ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py + --output + ${DEVTOOLS_SOURCE_DIR}/node_modules/karma/bin/karma start + ${CMAKE_CURRENT_BINARY_DIR}/karma.conf.js + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS SymbolsBackendTests SymbolsBackend LLDBEvalTests + ${TS_COMPILER_OUTPUTS} + ${EXTENSION_BUNDLED_SOURCES} + ) + +include_directories( + ${REPO_SOURCE_DIR}/third_party/llvm/src/llvm/utils/unittest/googlemock/include + ${REPO_SOURCE_DIR}/third_party/llvm/src/llvm/utils/unittest/googletest/include) + +set(LLVM_LINK_COMPONENTS Support) + +add_llvm_unittest(SymbolsBackendTests WasmModule_test.cc) + +add_custom_command(OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackendTests.wasm.debug.wasm.dwp + COMMAND + ${LLVM_DWP} + ARGS + -e ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackendTests.wasm.debug.wasm + -o ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackendTests.wasm.debug.wasm.dwp + WORKING_DIRECTORY + ${PROJECT_BINARY_DIR} + DEPENDS + SymbolsBackendTests) + + if(CXX_DEBUGGING_USE_SPLIT_DWARF) + add_custom_target(SymbolsBackendTestsDwp ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackendTests.wasm.debug.wasm.dwp) +endif() + +set_target_properties(SymbolsBackendTests PROPERTIES EXCLUDE_FROM_ALL FALSE) +target_link_libraries(SymbolsBackendTests PRIVATE + DWARFSymbols + -sALLOW_MEMORY_GROWTH=1 + -sMODULARIZE=1 + -sENVIRONMENT=web + -sEXPORT_NAME=createModule + -sEXIT_RUNTIME=1 + -sEXPORT_ES6=1 + -s'EXTRA_EXPORTED_RUNTIME_METHODS=[\"FS\"]' + -s'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=[\"$$Browser\"]' + -sWASM_BIGINT + -sERROR_ON_UNDEFINED_SYMBOLS=0 + -sASSERTIONS=1 + -sSEPARATE_DWARF_URL=SymbolsBackendTests.wasm.debug.wasm) + +if (CXX_DEBUGGING_USE_SANITIZERS) + target_link_libraries(SymbolsBackendTests PRIVATE + -sINITIAL_MEMORY=134217728 + -fsanitize=address,undefined + ) +endif() + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") + target_link_libraries(SymbolsBackendTests PRIVATE + -sERROR_ON_WASM_CHANGES_AFTER_LINK + -sREVERSE_DEPS=all + ) +endif() + +get_target_property(incdirs LLVMSupport INCLUDE_DIRECTORIES) +set(incflags) +foreach(inc IN LISTS incdirs) + list(APPEND incflags -I${inc}) +endforeach() + +add_executable(LLDBEvalTests + ${THIRD_PARTY_DIR}/lldb-eval/src/lldb-eval/eval_test.cc +) +# Set link and compile options directly to avoid accidentally inheriting +# global flags. +set(compile_options + -include ${CMAKE_CURRENT_SOURCE_DIR}/LLDBEvalExtensions.h + ${incflags} +) +set(link_options + -sASYNCIFY + -sASYNCIFY_STACK_SIZE=49152 + -sWASM_BIGINT + -std=c++17 + --bind + -g0 + -sEXPORT_NAME=loadModule + -sEXPORT_ES6=1 + -sASSERTIONS=1 +) +if (CXX_DEBUGGING_USE_SANITIZERS) + list(APPEND link_options + -fsanitize=address,undefined + -sINITIAL_MEMORY=134217728 + ) + list(APPEND compile_options + -fsanitize=address,undefined + ) +endif() + +set_target_properties(LLDBEvalTests PROPERTIES + COMPILE_OPTIONS "${compile_options}" + LINK_OPTIONS "${link_options}") +target_link_libraries(LLDBEvalTests PUBLIC llvm_gtest) +get_target_property(link_options LLDBEvalTests + LINK_OPTIONS ) +list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/LLDBEvalTests.js) +list(APPEND compiled_wasm_inputs ${CMAKE_CURRENT_BINARY_DIR}/LLDBEvalTests.wasm) + +add_dependencies(SymbolsBackendTests SymbolsBackendTestInputs TypescriptOutput) + +set(EXTENSION_TEST_BUILD_ARTIFACTS + ${TS_COMPILER_OUTPUTS} + ${EXTENSION_BUNDLED_SOURCES} + $ + $/LLDBEvalTests.wasm + $ + $/SymbolsBackendTests.wasm + $/SymbolsBackendTests.wasm.debug.wasm + $ + $/SymbolsBackend.wasm + $/SymbolsBackend.wasm.debug.wasm + ) + if(CXX_DEBUGGING_USE_SPLIT_DWARF AND NOT CXX_DEBUGGING_DWO_ONLY) + list(APPEND EXTENSION_TEST_BUILD_ARTIFACTS + $/SymbolsBackendTests.wasm.debug.wasm.dwp + $/SymbolsBackend.wasm.debug.wasm.dwp + ) +endif() + +add_subdirectory(inputs) + +configure_file(build-artifacts.js.in build-artifacts.js.in @ONLY) +file(GENERATE OUTPUT build-artifacts.js + INPUT ${CMAKE_CURRENT_BINARY_DIR}/build-artifacts.js.in) + +configure_file(karma.conf.in.js karma.conf.in.js @ONLY) +file(GENERATE OUTPUT karma.conf.js + INPUT ${CMAKE_CURRENT_BINARY_DIR}/karma.conf.in.js) diff --git a/extensions/cxx_debugging/tests/CustomFormatters_test.ts b/extensions/cxx_debugging/tests/CustomFormatters_test.ts new file mode 100644 index 0000000..c9bb1e5 --- /dev/null +++ b/extensions/cxx_debugging/tests/CustomFormatters_test.ts @@ -0,0 +1,307 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {MemorySlice, PageStore} from '../src/CustomFormatters.js'; + +function asArray(slice: MemorySlice): number[] { + return Array.from(new Uint8Array(slice.buffer)); +} + +describe('PageStore', () => { + it('merges slices correctly', () => { + const bufferA = new Uint8Array([1, 2, 3, 4]).buffer; + const bufferB = new Uint8Array([5, 6, 7, 8]).buffer; + + expect(() => new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 32))) + .to.throw('Slices are not contiguous'); + expect(() => new MemorySlice(bufferA, 32).merge(new MemorySlice(bufferB, 16))) + .to.throw('Slices are not contiguous'); + expect(asArray(new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 20)))).to.deep.equal([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ]); + expect(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 16)))).to.deep.equal([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ]); + expect(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 16)))).to.deep.equal([ + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + ]); + expect(asArray(new MemorySlice(bufferB, 16).merge(new MemorySlice(bufferA, 20)))).to.deep.equal([ + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + ]); + expect(asArray(new MemorySlice(bufferA, 18).merge(new MemorySlice(bufferB, 20)))).to.deep.equal([1, 2, 3, 4, 7, 8]); + expect(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 18)))).to.deep.equal([1, 2, 3, 4, 7, 8]); + expect(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 18)))).to.deep.equal([5, 6, 7, 8, 3, 4]); + expect(asArray(new MemorySlice(bufferB, 18).merge(new MemorySlice(bufferA, 20)))).to.deep.equal([5, 6, 7, 8, 3, 4]); + }); + + it('sorts disjoint slices correctly', () => { + const two = new Uint8Array([1, 2]).buffer; + + { + const view = new PageStore(); + view.addSlice(two, 2); + view.addSlice(two, 5); + view.addSlice(two, 8); + view.addSlice(two, 11); + view.addSlice(two, 14); + expect(view.slices.map(s => s.begin)).to.deep.equal([2, 5, 8, 11, 14]); + } + { + const view = new PageStore(); + view.addSlice(two, 14); + view.addSlice(two, 11); + view.addSlice(two, 8); + view.addSlice(two, 5); + view.addSlice(two, 2); + expect(view.slices.map(s => s.begin)).to.deep.equal([2, 5, 8, 11, 14]); + } + }); + + it('finds slice indices correctly', () => { + const four = new Uint8Array([3, 4, 5, 6]).buffer; + const view = new PageStore(); + expect(view.findSliceIndex(100)).to.eql(-1); + + for (const offset of [2, 7, 12, 17, 22]) { + view.addSlice(four, offset); + expect(view.findSliceIndex(0)).to.eql(-1); + expect(view.findSliceIndex(100)).to.eql(view.slices.length - 1); + for (let s = 0; s < view.slices.length; ++s) { + const slice = view.slices[s]; + + expect(view.findSliceIndex(slice.begin - 1)).to.eql(s - 1); + expect(view.findSliceIndex(slice.begin)).to.eql(s); + expect(view.findSliceIndex(slice.begin + 1)).to.eql(s); + expect(view.findSliceIndex(slice.end - 1)).to.eql(s); + expect(view.findSliceIndex(slice.end)).to.eql(s); + } + } + }); + + it('finds offsets correctly', () => { + const four = new Uint8Array([3, 4, 5, 6]).buffer; + const view = new PageStore(); + expect(view.findSlice(100)).to.eql(null); + + for (const offset of [2, 7, 12, 17, 22]) { + view.addSlice(four, offset); + expect(view.findSlice(100)).to.eql(null); + for (const slice of view.slices) { + expect(view.findSlice(slice.begin - 1)).to.eql(null); + expect(view.findSlice(slice.begin)).to.eql(slice); + expect(view.findSlice(slice.begin + 1)).to.eql(slice); + expect(view.findSlice(slice.end - 1)).to.eql(slice); + expect(view.findSlice(slice.end)).to.eql(null); + } + } + }); + + it('merges overlapping slices correctly', () => { + { + const view = new PageStore(); + view.addSlice([1, 2], 2); + view.addSlice([2, 3], 3); + expect(view.slices.length).to.eql(1); + expect(asArray(view.slices[0])).to.deep.equal([1, 2, 3]); + + view.addSlice([0, 1, 2, 3, 4], 1); + expect(view.slices.length).to.eql(1); + expect(asArray(view.slices[0])).to.deep.equal([0, 1, 2, 3, 4]); + } + + const getView = (): PageStore => { + const view = new PageStore(); + // --XX--XX--XX--XX + view.addSlice([1, 2], 2); + view.addSlice([4, 5], 6); + view.addSlice([7, 8], 10); + view.addSlice([10, 11], 14); + return view; + }; + + { + const view = getView(); + // --XX--XX--XX--XX + // || + view.addSlice([5, 4], 5); + expect(view.slices.length).to.eql(4); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([5, 4, 5]); + expect(view.slices[1].begin).to.eql(5); + expect(asArray(view.slices[2])).to.deep.equal([7, 8]); + expect(view.slices[2].begin).to.eql(10); + expect(asArray(view.slices[3])).to.deep.equal([10, 11]); + expect(view.slices[3].begin).to.eql(14); + } + + { + const view = getView(); + // --XX--XX--XX--XX + // || + view.addSlice([4, 5], 6); + expect(view.slices.length).to.eql(4); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([7, 8]); + expect(view.slices[2].begin).to.eql(10); + expect(asArray(view.slices[3])).to.deep.equal([10, 11]); + expect(view.slices[3].begin).to.eql(14); + } + + { + const view = getView(); + // --XX--XX--XX--XX + // || + view.addSlice([5, 6], 7); + expect(view.slices.length).to.eql(4); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5, 6]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([7, 8]); + expect(view.slices[2].begin).to.eql(10); + expect(asArray(view.slices[3])).to.deep.equal([10, 11]); + expect(view.slices[3].begin).to.eql(14); + } + + { + const view = getView(); + view.addSlice([20, 21], 20); + // --XX--XX--XX--XX----XX + // || + view.addSlice([17, 18], 17); + expect(view.slices.length).to.eql(6); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([7, 8]); + expect(view.slices[2].begin).to.eql(10); + expect(asArray(view.slices[3])).to.deep.equal([10, 11]); + expect(view.slices[3].begin).to.eql(14); + + expect(asArray(view.slices[4])).to.deep.equal([17, 18]); + expect(view.slices[4].begin).to.eql(17); + expect(asArray(view.slices[5])).to.deep.equal([20, 21]); + expect(view.slices[5].begin).to.eql(20); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |______| + view.addSlice([0, 0, 4, 5, 0, 0, 7, 8], 4); + expect(view.slices.length).to.eql(2); + expect(asArray(view.slices[0])).to.deep.equal([1, 2, 0, 0, 4, 5, 0, 0, 7, 8]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([10, 11]); + expect(view.slices[1].begin).to.eql(14); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |_____| + view.addSlice([0, 4, 5, 0, 0, 7, 8], 5); + expect(view.slices.length).to.eql(3); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([0, 4, 5, 0, 0, 7, 8]); + expect(view.slices[1].begin).to.eql(5); + expect(asArray(view.slices[2])).to.deep.equal([10, 11]); + expect(view.slices[2].begin).to.eql(14); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |___| + view.addSlice([5, 0, 0, 7, 8], 7); + expect(view.slices.length).to.eql(3); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([10, 11]); + expect(view.slices[2].begin).to.eql(14); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |____| + view.addSlice([4, 5, 0, 0, 7, 8], 6); + expect(view.slices.length).to.eql(3); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([10, 11]); + expect(view.slices[2].begin).to.eql(14); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |_____| + view.addSlice([4, 5, 0, 0, 7, 8, 0], 6); + expect(view.slices.length).to.eql(3); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8, 0]); + expect(view.slices[1].begin).to.eql(6); + expect(asArray(view.slices[2])).to.deep.equal([10, 11]); + expect(view.slices[2].begin).to.eql(14); + } + + { + const view = getView(); + + // --XX--XX--XX--XX + // |______| + view.addSlice([4, 5, 0, 0, 7, 8, 0, 0], 6); + expect(view.slices.length).to.eql(2); + expect(asArray(view.slices[0])).to.deep.equal([1, 2]); + expect(view.slices[0].begin).to.eql(2); + expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8, 0, 0, 10, 11]); + expect(view.slices[1].begin).to.eql(6); + } + }); +}); diff --git a/extensions/cxx_debugging/tests/Formatters_test.ts b/extensions/cxx_debugging/tests/Formatters_test.ts new file mode 100644 index 0000000..b49a816 --- /dev/null +++ b/extensions/cxx_debugging/tests/Formatters_test.ts @@ -0,0 +1,284 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {CustomFormatters, type TypeInfo} from '../src/CustomFormatters.js'; +import * as Formatters from '../src/Formatters.js'; + +import {TestValue, TestWasmInterface} from './TestUtils.js'; + +describe('Formatters', () => { + it('formatChar', () => { + const wasm = new TestWasmInterface(); + + const chars = [0x0, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0x19, 0x20, 0x7e, 0x7f, 0x21, 0x7d]; + const expectation = [ + '\\0', + '\\a', + '\\b', + '\\t', + '\\n', + '\\v', + '\\f', + '\\r', + '\\x19', + ' ', + '~', + '\\x7f', + '!', + '}', + ].map(c => `'${c}'`).join(); + + assert.deepEqual(chars.map(c => Formatters.formatChar(wasm, TestValue.fromInt8(c))).join(), expectation); + assert.deepEqual(chars.map(c => Formatters.formatChar(wasm, TestValue.fromUint8(c))).join(), expectation); + }); + + it('formatCStrings', () => { + const wasm = new TestWasmInterface(); + + // 0x0 + const ptr = TestValue.fromUint32(0, 'char *'); + assert.deepEqual(Formatters.formatCString(wasm, ptr), {'0x0': null}); + assert.deepEqual(Formatters.formatU16CString(wasm, ptr), {'0x0': null}); + assert.deepEqual(Formatters.formatCWString(wasm, ptr), {'0x0': null}); + // short string + + const shortString = 'abcdef\0'; + const shortStringValue = + new TestValue(new DataView(new TextEncoder().encode(shortString).buffer as ArrayBuffer), 'char'); + assert.deepEqual( + Formatters.formatCString(wasm, TestValue.pointerTo(shortStringValue, Formatters.Constants.SAFE_HEAP_START)), + 'abcdef'); + + const shortContents = new DataView(new ArrayBuffer(shortString.length * Uint32Array.BYTES_PER_ELEMENT)); + for (let i = 0; i < shortString.length; ++i) { + shortContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, shortString.codePointAt(i) ?? 0, true); + } + const shortWString = new TestValue(shortContents, 'wchar_t'); + assert.deepEqual( + Formatters.formatCWString(wasm, TestValue.pointerTo(shortWString, Formatters.Constants.SAFE_HEAP_START)), + 'abcdef'); + + // long string + const longString = `${new Array(Formatters.Constants.PAGE_SIZE / 4).fill('abcdefg').join('')}\0`; + const longStringValue = + new TestValue(new DataView(new TextEncoder().encode(longString).buffer as ArrayBuffer), 'char'); + assert.deepEqual( + Formatters.formatCString(wasm, TestValue.pointerTo(longStringValue, Formatters.Constants.SAFE_HEAP_START)), + longString.substr(0, longString.length - 1)); + + const longContents = new DataView(new ArrayBuffer(longString.length * Uint32Array.BYTES_PER_ELEMENT)); + for (let i = 0; i < longString.length; ++i) { + longContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, longString.codePointAt(i) ?? 0, true); + } + const longWString = new TestValue(longContents, 'wchar_t'); + assert.deepEqual( + Formatters.formatCWString(wasm, TestValue.pointerTo(longWString, Formatters.Constants.SAFE_HEAP_START)), + longString.substr(0, longString.length - 1)); + }); + + it('formatLibCXXString', () => { + const wasm = new TestWasmInterface(); + // short string + const shortString = 'abcdefgh'; + const shortFlag = TestValue.fromUint8(shortString.length); + const longString = new Array(128 / shortString.length).fill(shortString).join(''); + const longFlag = TestValue.fromUint8(0x80); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const __s_union = TestValue.fromMembers('__s_union', {}); + // eslint-disable-next-line @typescript-eslint/naming-convention + const __s = TestValue.fromMembers('__s', {'': __s_union}); + // eslint-disable-next-line @typescript-eslint/naming-convention + const __l = TestValue.fromMembers('__l', {}); + const str = TestValue.fromMembers('std::string', { + __r_: TestValue.fromMembers('__r_', { + __value_: TestValue.fromMembers('__value_', {'': TestValue.fromMembers('__value_union', {__s, __l})}), + }), + }); + + // short char8_t + __s.members.__data_ = + new TestValue(new DataView(new TextEncoder().encode(shortString).buffer as ArrayBuffer), 'char'); + __s_union.members.__size_ = shortFlag; + + assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), {size: shortString.length, string: shortString}); + + // long char8_t + const wideStringContents = new DataView(new ArrayBuffer(shortString.length * Uint32Array.BYTES_PER_ELEMENT)); + for (let i = 0; i < shortString.length; ++i) { + wideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, shortString.codePointAt(i) ?? 0, true); + } + __s.members.__data_ = new TestValue(wideStringContents, 'wchar_t'); + __s_union.members.__size_ = shortFlag; + + assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), {size: shortString.length, string: shortString}); + + // long char8_t + wasm.memory = new ArrayBuffer(Formatters.Constants.SAFE_HEAP_START + longString.length); + new Uint8Array(wasm.memory).set(new TextEncoder().encode(longString), Formatters.Constants.SAFE_HEAP_START); + __l.members.__data_ = TestValue.pointerTo( + new TestValue(new DataView(wasm.memory, Formatters.Constants.SAFE_HEAP_START), 'char'), + Formatters.Constants.SAFE_HEAP_START); + __s_union.members.__size_ = longFlag; + __l.members.__size_ = TestValue.fromUint32(longString.length); + + assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), {size: longString.length, string: longString}); + + // long char32_t + wasm.memory = + new ArrayBuffer(Formatters.Constants.SAFE_HEAP_START + longString.length * Uint32Array.BYTES_PER_ELEMENT); + const longWideStringContents = new DataView(wasm.memory, Formatters.Constants.SAFE_HEAP_START); + for (let i = 0; i < longString.length; ++i) { + longWideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, longString.codePointAt(i) ?? 0, true); + } + __l.members.__data_ = + TestValue.pointerTo(new TestValue(longWideStringContents, 'wchar_t'), Formatters.Constants.SAFE_HEAP_START); + __s_union.members.__size_ = longFlag; + __l.members.__size_ = TestValue.fromUint32(longString.length); + + assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), {size: longString.length, string: longString}); + }); + + it('formatVector', () => { + const wasm = new TestWasmInterface(); + const elements = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(v => TestValue.fromFloat32(v)); + const __begin_ = TestValue.pointerTo(elements, 0x1234); // eslint-disable-line @typescript-eslint/naming-convention + const __end_ = // eslint-disable-line @typescript-eslint/naming-convention + TestValue.pointerTo(elements[elements.length - 1], 0x1234 + Float32Array.BYTES_PER_ELEMENT * elements.length); + const vector = new TestValue(new DataView(new ArrayBuffer(0)), 'std::vector', {__begin_, __end_}); + + assert.deepEqual(Formatters.formatVector(wasm, vector), elements); + }); + + it('formatPointerOrReference', () => { + const wasm = new TestWasmInterface(); + assert.deepEqual(Formatters.formatPointerOrReference(wasm, TestValue.fromUint32(0)), {'0x0': null}); + + const pointee = TestValue.fromFloat64(15); + + assert.deepEqual( + Formatters.formatPointerOrReference(wasm, TestValue.pointerTo(pointee, 0x1234)), {'0x1234': pointee}); + }); + + it('formatDynamicArray', () => { + const wasm = new TestWasmInterface(); + const element = TestValue.fromFloat32(5); + const array = new TestValue(element.asDataView(), 'float[]', {0: element}); + array.location = 0x1234; + assert.deepEqual(Formatters.formatDynamicArray(wasm, array), {'0x1234': element}); + }); + + it('formatUint128', () => { + const wasm = new TestWasmInterface(); + const high = 0xdeadn; + const low = 0xbeefbeefbeefbeefn; + const content = new DataView(new BigUint64Array([low, high]).buffer); + + const actual = Formatters.formatUInt128(wasm, new TestValue(content, 'uint128_t')); + const expected = 0xdeadbeefbeefbeefbeefn; + assert.deepEqual(actual, expected, `expected 0x${actual.toString(16)} to equal 0x${expected.toString(16)}`); + }); + + it('formatInt128', () => { + const wasm = new TestWasmInterface(); + const expected = -0xdeadbeefbeefbeefbeefn; + const high = expected >> 64n; + const low = expected & 0xffffffffffffffffn; + const content = new DataView(new BigInt64Array([low, high]).buffer); + + const actual = Formatters.formatInt128(wasm, new TestValue(content, 'int128_t')); + assert.deepEqual(actual, expected, `expected 0x${actual.toString(16)} to equal 0x${expected.toString(16)}`); + }); + + it('formatVoid', async () => { + const actual = await Formatters.formatVoid()().asRemoteObject(); + assert.deepEqual(actual, { + type: 'undefined', + value: undefined, + description: '', + hasChildren: false, + linearMemorySize: undefined, + linearMemoryAddress: undefined, + }); + }); +}); + +describe('CustomFormatters', () => { + for (const makeConst of [true, false]) { + it(`looks up formatters correctly ${makeConst ? 'const' : 'non-const'} type`, () => { + const type = (typeName: string, typeProperties: Partial = {}): TypeInfo => { + if (makeConst) { + typeName = `const ${typeName}`; + } + return { + typeNames: [typeName], + typeId: typeName, + members: [], + alignment: 0, + arraySize: 0, + size: 0, + isPointer: Boolean(typeName.match(/^.+\*$/) || typeName.match(/^.+&$/)), + canExpand: false, + hasValue: false, + ...typeProperties, + }; + }; + + assert.deepEqual(CustomFormatters.get(type('std::__2::string'))?.format, Formatters.formatLibCXX8String); + assert.deepEqual( + CustomFormatters + .get(type('std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX8String); + assert.deepEqual(CustomFormatters.get(type('std::__2::u8string'))?.format, Formatters.formatLibCXX8String); + assert.deepEqual( + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX8String); + + assert.deepEqual(CustomFormatters.get(type('std::__2::u16string'))?.format, Formatters.formatLibCXX16String); + assert.deepEqual( + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX16String); + + assert.deepEqual(CustomFormatters.get(type('std::__2::wstring'))?.format, Formatters.formatLibCXX32String); + assert.deepEqual( + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX32String); + assert.deepEqual(CustomFormatters.get(type('std::__2::u32string'))?.format, Formatters.formatLibCXX32String); + assert.deepEqual( + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX32String); + + assert.deepEqual(CustomFormatters.get(type('char *'))?.format, Formatters.formatCString); + assert.deepEqual(CustomFormatters.get(type('char8_t *'))?.format, Formatters.formatCString); + assert.deepEqual(CustomFormatters.get(type('char16_t *'))?.format, Formatters.formatU16CString); + assert.deepEqual(CustomFormatters.get(type('wchar_t *'))?.format, Formatters.formatCWString); + assert.deepEqual(CustomFormatters.get(type('char32_t *'))?.format, Formatters.formatCWString); + assert.deepEqual( + CustomFormatters.get(type('int (*)()', {isPointer: true}))?.format, Formatters.formatPointerOrReference); + + assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); + assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); + + assert.deepEqual(CustomFormatters.get(type('int *'))?.format, Formatters.formatPointerOrReference); + assert.deepEqual(CustomFormatters.get(type('int &'))?.format, Formatters.formatPointerOrReference); + assert.deepEqual(CustomFormatters.get(type('int[]'))?.format, Formatters.formatDynamicArray); + + assert.deepEqual(CustomFormatters.get(type('unsigned __int128'))?.format, Formatters.formatUInt128); + assert.deepEqual(CustomFormatters.get(type('__int128'))?.format, Formatters.formatInt128); + }); + } +}); diff --git a/extensions/cxx_debugging/tests/Interpreter_test.ts b/extensions/cxx_debugging/tests/Interpreter_test.ts new file mode 100644 index 0000000..4efe7c9 --- /dev/null +++ b/extensions/cxx_debugging/tests/Interpreter_test.ts @@ -0,0 +1,219 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import {createEmbindPool} from '../src/DWARFSymbols.js'; + +import type * as LLDBEvalTests from './LLDBEvalTests.js'; +import loadModule from './LLDBEvalTests.js'; +import {Debugger} from './RealBackend.js'; +import {createWorkerPlugin, makeURL, remoteObject} from './TestUtils.js'; + +const WASM_URL = makeURL('/build/tests/inputs/lldb_eval_inputs.wasm'); +class LLDBEvalDebugger implements LLDBEvalTests.Debugger { + #debugger: Debugger; + #plugin: Chrome.DevTools.LanguageExtensionPlugin; + constructor(dbg: Debugger, plugin: Chrome.DevTools.LanguageExtensionPlugin) { + this.#debugger = dbg; + this.#plugin = plugin; + } + + static async create(): Promise { + const dbg = await Debugger.create(); + const plugin = await createWorkerPlugin(dbg); + return new LLDBEvalDebugger(dbg, plugin); + } + + private async stringify(result: Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject): Promise { + if (!this.#plugin.getProperties) { + throw new Error('getProperties not implemented'); + } + if (result.type === 'reftype') { + return 'reftype'; + } + if (result.objectId) { + const properties = await this.#plugin.getProperties(result.objectId); + if (properties.length === 1) { + const [{name}] = properties; + if (name.startsWith('0x')) { + return `0x${name.substring(2).padStart(8, '0')}`; + } + } + } + if (result.description === 'std::nullptr_t') { + return '0x00000000'; + } + if (Object.is(result.value, -0)) { + return '-0'; + } + if (result.value === -Infinity) { + return '-Inf'; + } + if (result.value === Infinity) { + return '+Inf'; + } + + return result.description ?? `${result.value}`; + } + + async evaluate(expr: string): Promise { + const {callFrame, rawLocation} = await this.#debugger.waitForPause(); + if (!this.#plugin.evaluate) { + throw new Error('Not implemented'); + } + try { + const resultObject = await this.#plugin.evaluate(expr, rawLocation, this.#debugger.stopIdForCallFrame(callFrame)); + if (!resultObject) { + return {error: `Could not evaluate expression '${expr}'`}; + } + const result = await this.stringify(resultObject); + return {result}; + + } catch (e) { + return {error: `${e}`}; + } + } + + async exit(): Promise { + if (this.#debugger.isPaused()) { + await this.#debugger.clearBreakpoints(); + await this.#debugger.resume(); + const rawModuleId = await this.#debugger.waitForScript(WASM_URL); + await this.#plugin.removeRawModule(rawModuleId); + } + } + + async runToLine(line: string): Promise { + const page = this.#debugger.page('./lldb_eval_inputs.js'); + await page.open(); + + const rawModuleId = await this.#debugger.waitForScript(WASM_URL); + + const url = makeURL('/build/tests/inputs/lldb_eval_inputs.wasm.debug.wasm'); + const sources = await this.#plugin.addRawModule(rawModuleId, '', {url}); + if ('missingSymbolFiles' in sources) { + throw new Error('Unexpected missing symbol files'); + } + const sourceFileURL = sources.find(s => s.endsWith('test_binary.cc')); + if (!sourceFileURL) { + throw new Error('test_binary.cc source not found'); + } + + const breakpoint = + await this.#debugger.setBreakpointOnSourceLine(line, new URL(sourceFileURL), this.#plugin, rawModuleId); + + const goPromise = page.go(); + const pauseOrExitcode = await Promise.race([goPromise, this.#debugger.waitForPause()]); + if (typeof pauseOrExitcode === 'number') { + throw new Error('Program terminated before all breakpoints were hit.'); + } + + const {rawLocation} = pauseOrExitcode; + const [sourceLocation] = await this.#plugin.rawLocationToSourceLocation(rawLocation); + if (sourceLocation?.lineNumber !== breakpoint.lineNumber) { + throw new Error( + `Paused on unexpected line ${sourceLocation?.lineNumber}. Breakpoint was set on ${breakpoint.lineNumber}.`); + } + } + + close(): Promise { + return this.#debugger.close(); + } +} + +describe('Interpreter', () => { + it('passes the lldb-eval test suite.', async () => { + const lldbEval = await loadModule(); + const debug = await LLDBEvalDebugger.create(); + + const {manage, flush} = createEmbindPool(); + try { + const argv = manage(new lldbEval.StringArray()); + + const skippedTests = [ + 'EvalTest.TestTemplateTypes', + 'EvalTest.TestUnscopedEnumNegation', + 'EvalTest.TestUniquePtrDeref', + 'EvalTest.TestUniquePtrCompare', + ]; + argv.push_back(`--gtest_filter=-${skippedTests.join(':')}`); + + const exitCode = await lldbEval.runTests(debug, argv); + assert.strictEqual(exitCode, 0, 'gtest test suite failed'); + } finally { + flush(); + } + }); + + it('can do basic arithmetic.', async () => { + const debug = await Debugger.create(); + const page = debug.page('./addresses_main.js'); + await page.open(); + + const wasmUrl = makeURL('/build/tests/inputs/addresses_main.wasm'); + const rawModuleId = await debug.waitForScript(wasmUrl); + const plugin = await createWorkerPlugin(debug); + + const url = makeURL('/build/tests/inputs/addresses_main.wasm.debug.wasm'); + const sources = await plugin.addRawModule(rawModuleId, '', {url}); + if ('missingSymbolFiles' in sources) { + throw new Error('Unexpected missing symbol files'); + } + const sourceFileURL = sources.find(s => s.endsWith('addresses.cc')); + if (!sourceFileURL) { + throw new Error('addresses.cc source not found'); + } + + const {lineNumber} = await debug.setBreakpointOnSourceLine( + '// BREAK(ArrayMembersTest)', new URL(sourceFileURL), plugin, rawModuleId); + + const goPromise = page.go(); + const pauseOrExitcode = await Promise.race([debug.waitForPause(), goPromise]); + if (typeof pauseOrExitcode === 'number') { + throw new Error('Program terminated before all breakpoints were hit.'); + } + + const {callFrame, rawLocation} = pauseOrExitcode; + + const [sourceLocation] = await plugin.rawLocationToSourceLocation(rawLocation); + if (sourceLocation?.lineNumber !== lineNumber) { + throw new Error('Paused at an unexpected location. Have not set a breakpoint here.'); + } + + const variables = await plugin.listVariablesInScope(rawLocation); + expect(variables.map(v => v.name).sort()).to.deep.equal(['n', 'sum', 'x']); + + if (!plugin.evaluate) { + throw new Error('evaluate is undefined'); + } + + { + const {value} = remoteObject(await plugin.evaluate('n + sum', rawLocation, debug.stopIdForCallFrame(callFrame))); + expect(value).to.eql(55); + } + { + const {value} = + remoteObject(await plugin.evaluate('(wchar_t)0x41414141', rawLocation, debug.stopIdForCallFrame(callFrame))); + expect(value).to.eql('U+41414141'); + } + { + const {value} = + remoteObject(await plugin.evaluate('(char16_t)0x4141', rawLocation, debug.stopIdForCallFrame(callFrame))); + expect(value).to.eql('䅁'); + } + { + const {value} = + remoteObject(await plugin.evaluate('(char32_t)0x41414141', rawLocation, debug.stopIdForCallFrame(callFrame))); + expect(value).to.eql('U+41414141'); + } + { + const {value} = + remoteObject(await plugin.evaluate('(char32_t)0x4141', rawLocation, debug.stopIdForCallFrame(callFrame))); + expect(value).to.eql('䅁'); + } + + await debug.resume(); + await debug.close(); + }); +}); diff --git a/extensions/cxx_debugging/tests/LLDBEvalExtensions.h b/extensions/cxx_debugging/tests/LLDBEvalExtensions.h new file mode 100644 index 0000000..fcbf2ad --- /dev/null +++ b/extensions/cxx_debugging/tests/LLDBEvalExtensions.h @@ -0,0 +1,154 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include "emscripten.h" +#include "emscripten/bind.h" +#include "emscripten/val.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#define HAS_METHOD(...) (true) + +using ::testing::Matcher; +using ::testing::MatcherInterface; +using ::testing::MatchResultListener; + +void PrintError(::testing::MatchResultListener* listener, + const std::string& error); + +static emscripten::val debugger = emscripten::val::null(); + +int run(emscripten::val debuggerContext, std::vector args) { + if (!debuggerContext.as()) { + return -1; + } + debugger = debuggerContext; + char arg0[] = "LLDBEvalTests"; + std::vector argv({arg0}); + for (auto& arg : args) { + argv.push_back(arg.data()); + } + int argc = argv.size(); + + ::testing::InitGoogleTest(&argc, argv.data()); + return RUN_ALL_TESTS(); +} + +EMSCRIPTEN_BINDINGS(LLDBEval) { + emscripten::function("runTests", &run); + emscripten::register_vector("StringArray"); +} + +struct EvalResult { + std::string result; + llvm::Optional error; +}; + +class EvalTest : public ::testing::Test { + public: + void SetUp() { + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + std::string break_line = "// BREAK(" + test_name + ")"; + debugger.call("runToLine", break_line).await(); + } + + void TearDown() { debugger.call("exit").await(); } + + EvalResult Eval(const std::string& expr) const { + const auto evalResult = + debugger.call("evaluate", expr).await(); + const auto result = evalResult["result"]; + const auto error = evalResult["error"]; + return {result.as() ? result.as() : std::string(), + error.as() + ? llvm::Optional(error.as()) + : llvm::None}; + } + + bool Is32Bit() const { return true; } + + protected: + struct { + size_t GetAddressByteSize() const { return 4; } + } process_; + bool compare_with_lldb_ = false; // ignored +}; + +class IsEqualMatcher : public MatcherInterface { + public: + IsEqualMatcher(std::string value, bool compare_types) + : value_(std::move(value)) {} + + public: + bool MatchAndExplain(EvalResult result, + MatchResultListener* listener) const override { + if (result.error) { + PrintError(listener, *result.error); + return false; + } + + if (result.result != value_) { + *listener << "evaluated to '" << result.result << "'"; + return false; + } + + return true; + } + + void DescribeTo(std::ostream* os) const override { + *os << "evaluates to '" << value_ << "'"; + } + + private: + std::string value_; +}; + +class IsOkMatcher : public MatcherInterface { + public: + explicit IsOkMatcher(bool compare_types) {} + + bool MatchAndExplain(EvalResult result, + MatchResultListener* listener) const override { + if (result.error) { + PrintError(listener, *result.error); + return false; + } + return true; + } + + void DescribeTo(std::ostream* os) const override { + *os << "evaluates without an error"; + } +}; + +class IsErrorMatcher : public MatcherInterface { + public: + explicit IsErrorMatcher(std::string value) : value_(std::move(value)) {} + + public: + bool MatchAndExplain(EvalResult result, + MatchResultListener* listener) const override { + if (!result.error) { + *listener << "evaluated to '" << result.result << "'"; + return false; + } + if (result.error->find(value_) == std::string::npos) { + PrintError(listener, *result.error); + return false; + } + + return true; + } + + void DescribeTo(std::ostream* os) const override { + *os << "evaluates with an error: '" << value_ << "'"; + } + + private: + std::string value_; +}; diff --git a/extensions/cxx_debugging/tests/LLDBEvalTests.d.ts b/extensions/cxx_debugging/tests/LLDBEvalTests.d.ts new file mode 100644 index 0000000..cea4d69 --- /dev/null +++ b/extensions/cxx_debugging/tests/LLDBEvalTests.d.ts @@ -0,0 +1,24 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import type {Vector} from '../src/SymbolsBackend.js'; + +export interface Debugger { + runToLine(line: string): Promise; + evaluate(expr: string): Promise; + exit(): Promise; +} + +export interface EvalResult { + error?: string; + result?: string; +} + +interface Module extends EmscriptenModule { + // eslint-disable-next-line @typescript-eslint/naming-convention + StringArray: Vector; + runTests(dbg: Debugger, args: Vector): Promise; +} + +declare let loadModule: EmscriptenModuleFactory; +export default loadModule; diff --git a/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts b/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts new file mode 100644 index 0000000..7a72d7b --- /dev/null +++ b/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts @@ -0,0 +1,136 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {findModuleConfiguration, resolveSourcePathToURL} from '../src/ModuleConfiguration.js'; + +describe('findModuleConfiguration', () => { + const CONFIGA = {name: 'projectA.wasm', pathSubstitutions: []}; + const CONFIGB = {name: 'projectB.wasm', pathSubstitutions: []}; + const DEFAULT = {pathSubstitutions: [{from: '/foo', to: '/bar'}]}; + const URLA = new URL('http://localhost/projectA.wasm'); + const URLB = new URL('file:/C:/Users/Admin/Projects/projectB.wasm'); + const URLC = new URL('https://web.dev/projectC.wasm'); + + it('correctly yields named configuration', () => { + expect(findModuleConfiguration([CONFIGA], URLA)).to.be.equal(CONFIGA); + expect(findModuleConfiguration([CONFIGA, CONFIGB], URLA)).to.be.equal(CONFIGA); + expect(findModuleConfiguration([CONFIGB, CONFIGA], URLA)).to.be.equal(CONFIGA); + expect(findModuleConfiguration([DEFAULT, CONFIGB, CONFIGA], URLB)).to.be.equal(CONFIGB); + expect(findModuleConfiguration([CONFIGB, CONFIGA, DEFAULT], URLB)).to.be.equal(CONFIGB); + }); + + it('correctly yields default configuration', () => { + expect(findModuleConfiguration([CONFIGA], URLB)).to.deep.equal({pathSubstitutions: []}); + expect(findModuleConfiguration([CONFIGA, CONFIGB], URLC)).to.deep.equal({pathSubstitutions: []}); + expect(findModuleConfiguration([CONFIGB, CONFIGA], URLC)).to.deep.equal({pathSubstitutions: []}); + expect(findModuleConfiguration([DEFAULT, CONFIGB, CONFIGA], URLC)).to.be.equal(DEFAULT); + expect(findModuleConfiguration([CONFIGB, CONFIGA, DEFAULT], URLC)).to.be.equal(DEFAULT); + }); + + it('correctly matches basename *-patterns', () => { + const STAR_WASM = {name: '*.wasm', pathSubstitutions: []}; + const PROJECTA_STAR = {name: 'projectA.*', pathSubstitutions: []}; + const PROJECT_STAR_WASM = {name: 'project*.wasm', pathSubstitutions: []}; + expect(findModuleConfiguration([PROJECTA_STAR], URLA)).to.be.equal(PROJECTA_STAR); + expect(findModuleConfiguration([STAR_WASM, PROJECTA_STAR], URLA)).to.be.equal(STAR_WASM); + expect(findModuleConfiguration([STAR_WASM, PROJECTA_STAR], new URL('http://example.com/projectA.foo'))) + .to.be.equal(PROJECTA_STAR); + expect(findModuleConfiguration([PROJECT_STAR_WASM], URLA)).to.be.equal(PROJECT_STAR_WASM); + expect(findModuleConfiguration([PROJECT_STAR_WASM], URLB)).to.be.equal(PROJECT_STAR_WASM); + }); + + it('corectly matches **-patterns', () => { + const PROJECT = {name: 'http://localhost/**/*.wasm', pathSubstitutions: []}; + expect(findModuleConfiguration([PROJECT], new URL('http://localhost/file.wasm'))).to.be.equal(PROJECT); + }); +}); + +describe('resolveSourcePathToURL', () => { + it('correctly resolves absolute paths', () => { + const BASE_URL = new URL('http://localhost/file.wasm'); + expect(resolveSourcePathToURL([], '/', BASE_URL).href).to.equal('file:///'); + expect(resolveSourcePathToURL([], '/usr/local', BASE_URL).href).to.equal('file:///usr/local'); + expect(resolveSourcePathToURL([], '/Users/Administrator', BASE_URL).href).to.equal('file:///Users/Administrator'); + expect(resolveSourcePathToURL([], 'A:/', BASE_URL).href).to.equal('file:///A:/'); + expect(resolveSourcePathToURL([], 'c:\\', BASE_URL).href).to.equal('file:///c:/'); + expect(resolveSourcePathToURL([], 'c:\\Users\\Clippy\\Source', BASE_URL).href) + .to.equal('file:///c:/Users/Clippy/Source'); + expect(resolveSourcePathToURL([], '\\\\network\\Server\\Source', BASE_URL).href) + .to.equal('file://network/Server/Source'); + }); + + it('correctly resolves relative paths', () => { + expect(resolveSourcePathToURL([], 'stdint.h', new URL('http://localhost/file.wasm')).href) + .to.equal('http://localhost/stdint.h'); + expect(resolveSourcePathToURL([], 'emscripten/include/iostream', new URL('http://localhost/dist/module.wasm')).href) + .to.equal('http://localhost/dist/emscripten/include/iostream'); + expect(resolveSourcePathToURL([], './src/main.cc', new URL('https://www.example.com/fast.wasm')).href) + .to.equal('https://www.example.com/src/main.cc'); + expect(resolveSourcePathToURL([], '.\\Mein Projekt\\Datei.cpp', new URL('https://www.example.com/fast.wasm')).href) + .to.equal('https://www.example.com/Mein%20Projekt/Datei.cpp'); + }); + + it('correctly applies source path substitutions', () => { + const BASE_URL = new URL('http://localhost/file.wasm'); + expect(resolveSourcePathToURL([{from: '/usr/src', to: '/mnt/src'}], '/usr/include/stdio.h', BASE_URL).href) + .to.equal('file:///usr/include/stdio.h'); + expect(resolveSourcePathToURL([{from: '/usr/src', to: '/mnt/src'}], '/usr/src/include/stdio.h', BASE_URL).href) + .to.equal('file:///mnt/src/include/stdio.h'); + expect( + resolveSourcePathToURL( + [{from: '/usr/src', to: '/mnt/src'}, {from: '/mnt/src', to: '/foo'}], '/usr/src/include/stdio.h', BASE_URL) + .href) + .to.equal('file:///mnt/src/include/stdio.h'); + expect(resolveSourcePathToURL( + [{from: '/usr/src/include', to: '/mnt/include'}, {from: '/usr/src', to: '/mnt/src'}], + '/usr/src/include/stdio.h', BASE_URL) + .href) + .to.equal('file:///mnt/include/stdio.h'); + expect(resolveSourcePathToURL([{from: '.', to: '/srv/central/src'}], './include/string', BASE_URL).href) + .to.equal('file:///srv/central/src/include/string'); + }); + + it('correctly resolves the sidecar Wasm module path', () => { + // We use resolveSourcePathToURL() with an empty source + // map to locate the debugging sidecar Wasm module. + expect(resolveSourcePathToURL([], 'file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href) + .to.equal('http://localhost:8000/wasm/file.wasm.debug.wasm'); + expect( + resolveSourcePathToURL([], '/usr/local/file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')) + .href) + .to.equal('file:///usr/local/file.wasm.debug.wasm'); + expect(resolveSourcePathToURL( + [], 'f:\\netdrive\\file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')) + .href) + .to.equal('file:///f:/netdrive/file.wasm.debug.wasm'); + }); + + it('correctly deals with dot patterns', () => { + // Test that we match LLDB's behavior as implemented for `settings set target.source-map`: + // http://cs/github/llvm/llvm-project/lldb/source/Target/PathMappingList.cpp?l=157-185 + const BASE_URL = new URL('http://web.dev/file.wasm'); + expect(resolveSourcePathToURL([{from: '.', to: '/foo/bar'}], 'include/header.h', BASE_URL).href) + .to.equal('file:///foo/bar/include/header.h'); + expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo\\bar'}], 'include/header.h', BASE_URL).href) + .to.equal('file:///c:/foo/bar/include/header.h'); + expect(resolveSourcePathToURL([{from: '.', to: '/foo'}], '/mnt/main.c', BASE_URL).href) + .to.equal('file:///mnt/main.c'); + expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo'}], '/mnt/main.c', BASE_URL).href) + .to.equal('file:///mnt/main.c'); + expect(resolveSourcePathToURL([{from: '.', to: '/foo'}], 'c:\\mnt\\main.c', BASE_URL).href) + .to.equal('file:///c:/mnt/main.c'); + expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo'}], 'c:\\mnt\\main.c', BASE_URL).href) + .to.equal('file:///c:/mnt/main.c'); + }); + + it('correctly deals with dot patterns when the source path is a URL', () => { + const BASE_URL = new URL('http://web.dev/file.wasm'); + expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'file:///main.cpp', BASE_URL).href) + .to.equal('file:///main.cpp'); + expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'http://localhost/main.cpp', BASE_URL).href) + .to.equal('http://localhost/main.cpp'); + expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'https://localhost/main.cpp', BASE_URL).href) + .to.equal('https://localhost/main.cpp'); + }); +}); diff --git a/extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts b/extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts new file mode 100644 index 0000000..ce6f559 --- /dev/null +++ b/extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts @@ -0,0 +1,12 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export interface SymbolsBackendTestsModule extends EmscriptenModule { + // eslint-disable-next-line @typescript-eslint/naming-convention + FS: typeof FS; + callMain(args?: string[]): void; +} + +declare let createModule: EmscriptenModuleFactory; +export default createModule; diff --git a/extensions/cxx_debugging/tests/SymbolsBackend_test.ts b/extensions/cxx_debugging/tests/SymbolsBackend_test.ts new file mode 100644 index 0000000..57941e3 --- /dev/null +++ b/extensions/cxx_debugging/tests/SymbolsBackend_test.ts @@ -0,0 +1,39 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import createModule, {type SymbolsBackendTestsModule} from './SymbolsBackendTests.js'; + +describe('SymbolsBackend', () => { + it('should work', async () => { + await createModule({ + onExit(status: number) { + if (status !== 0) { + throw new Error(`Unittests failed (return code ${status})`); + } + }, + // @ts-expect-error + preRun({FS}: SymbolsBackendTestsModule) { // eslint-disable-line @typescript-eslint/naming-convention + FS.mkdir('tests'); + FS.mkdir('tests/inputs'); + FS.mkdir('cxx_debugging'); + FS.mkdir('cxx_debugging/tests'); + FS.mkdir('cxx_debugging/tests/inputs'); + ['hello.s.wasm', + 'windows_paths.s.wasm', + 'globals.s.wasm', + 'classstatic.s.wasm', + 'namespaces.s.wasm', + 'shadowing.s.wasm', + 'inline.s.wasm', + ] + .forEach( + name => FS.createPreloadedFile( + 'cxx_debugging/tests/inputs', name, `build/tests/inputs/${name}`, true, false)); + ['split-dwarf.s.dwo', + 'split-dwarf.s.wasm', + ].forEach(name => FS.createPreloadedFile('tests/inputs', name, `build/tests/inputs/${name}`, true, false)); + }, + }); + }); +}); diff --git a/extensions/cxx_debugging/tests/TestUtils.ts b/extensions/cxx_debugging/tests/TestUtils.ts new file mode 100644 index 0000000..c5f2e03 --- /dev/null +++ b/extensions/cxx_debugging/tests/TestUtils.ts @@ -0,0 +1,250 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import type {Value, WasmInterface} from '../src/CustomFormatters.js'; +import {WorkerPlugin} from '../src/DevToolsPluginHost.js'; +import type {WasmValue} from '../src/WasmTypes.js'; +import type {HostInterface} from '../src/WorkerRPC.js'; + +import type {Debugger} from './RealBackend.js'; + +export class TestHostInterface implements HostInterface { + getWasmLinearMemory(_offset: number, _length: number, _stopId: unknown): ArrayBuffer { + throw new Error('Method not implemented.'); + } + getWasmLocal(_local: number, _stopId: unknown): WasmValue { + throw new Error('Method not implemented.'); + } + getWasmGlobal(_global: number, _stopId: unknown): WasmValue { + throw new Error('Method not implemented.'); + } + getWasmOp(_op: number, _stopId: unknown): WasmValue { + throw new Error('Method not implemented.'); + } + reportResourceLoad( + _resourceUrl: string, + _status: {success: boolean, errorMessage?: string|undefined, size?: number|undefined}): Promise { + return Promise.resolve(); + } +} + +export function makeURL(path: string): string { + return new URL(path, document.baseURI).href; +} + +export async function createWorkerPlugin(debug?: Debugger): Promise { + return await WorkerPlugin.create([], true).then(p => { + if (debug) { + p.getWasmLinearMemory = debug.getWasmLinearMemory.bind(debug); + p.getWasmLocal = debug.getWasmLocal.bind(debug); + p.getWasmGlobal = debug.getWasmGlobal.bind(debug); + p.getWasmOp = debug.getWasmOp.bind(debug); + } + /* eslint-disable-next-line no-debugger */ + debugger; // Halt in the debugger to let developers set breakpoints in C++. + return p; + }); +} + +export function relativePathname(url: URL, base: URL): string { + const baseSplit = base.pathname.split('/'); + const urlSplit = url.pathname.split('/'); + + let i = 0; + for (; i < Math.min(baseSplit.length, urlSplit.length); ++i) { + if (baseSplit[i] !== urlSplit[i]) { + break; + } + } + const result = new Array(baseSplit.length - i); + result.fill('..'); + result.push(...urlSplit.slice(i).filter(p => p.length > 0)); + + return result.join('/'); +} + +export function nonNull(value: T|null|undefined): T { + assert.exists(value); + return value as T; +} + +export function remoteObject(value: Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject|null): + Chrome.DevTools.RemoteObject { + assert.exists(value); + assert(value.type !== 'reftype'); + return value; +} + +export class TestWasmInterface implements WasmInterface { + memory = new ArrayBuffer(0); + locals = new Map(); + globals = new Map(); + stack = new Map(); + + readMemory(offset: number, length: number): Uint8Array { + return new Uint8Array(this.memory, offset, length); + } + getOp(op: number): WasmValue { + const val = this.stack.get(op); + if (val !== undefined) { + return val; + } + throw new Error(`No stack entry ${op}`); + } + getLocal(local: number): WasmValue { + const val = this.locals.get(local); + if (val !== undefined) { + return val; + } + throw new Error(`No local ${local}`); + } + getGlobal(global: number): WasmValue { + const val = this.globals.get(global); + if (val !== undefined) { + return val; + } + throw new Error(`No global ${global}`); + } +} + +export class TestValue implements Value { + private dataView: DataView; + members: {[key: string]: TestValue, [key: number]: TestValue}; + location: number; + size: number; + typeNames: string[]; + + static fromInt8(value: number, typeName = 'int8_t'): TestValue { + const content = new DataView(new ArrayBuffer(1)); + content.setInt8(0, value); + return new TestValue(content, typeName); + } + static fromInt16(value: number, typeName = 'int16_t'): TestValue { + const content = new DataView(new ArrayBuffer(2)); + content.setInt16(0, value, true); + return new TestValue(content, typeName); + } + static fromInt32(value: number, typeName = 'int32_t'): TestValue { + const content = new DataView(new ArrayBuffer(4)); + content.setInt32(0, value, true); + return new TestValue(content, typeName); + } + static fromInt64(value: bigint, typeName = 'int64_t'): TestValue { + const content = new DataView(new ArrayBuffer(8)); + content.setBigInt64(0, value, true); + return new TestValue(content, typeName); + } + static fromUint8(value: number, typeName = 'uint8_t'): TestValue { + const content = new DataView(new ArrayBuffer(1)); + content.setUint8(0, value); + return new TestValue(content, typeName); + } + static fromUint16(value: number, typeName = 'uint16_t'): TestValue { + const content = new DataView(new ArrayBuffer(2)); + content.setUint16(0, value, true); + return new TestValue(content, typeName); + } + static fromUint32(value: number, typeName = 'uint32_t'): TestValue { + const content = new DataView(new ArrayBuffer(4)); + content.setUint32(0, value, true); + return new TestValue(content, typeName); + } + static fromUint64(value: bigint, typeName = 'uint64_t'): TestValue { + const content = new DataView(new ArrayBuffer(8)); + content.setBigUint64(0, value, true); + return new TestValue(content, typeName); + } + static fromFloat32(value: number, typeName = 'float'): TestValue { + const content = new DataView(new ArrayBuffer(4)); + content.setFloat32(0, value, true); + return new TestValue(content, typeName); + } + static fromFloat64(value: number, typeName = 'double'): TestValue { + const content = new DataView(new ArrayBuffer(8)); + content.setFloat64(0, value, true); + return new TestValue(content, typeName); + } + static pointerTo(pointeeOrElements: TestValue|TestValue[], address?: number): TestValue { + const content = new DataView(new ArrayBuffer(4)); + const elements = Array.isArray(pointeeOrElements) ? pointeeOrElements : [pointeeOrElements]; + address = address ?? elements[0].location; + content.setUint32(0, address, true); + const space = elements[0].typeNames[0].endsWith('*') ? '' : ' '; + const members: {[key: string|number]: TestValue} = {'*': elements[0]}; + for (let i = 0; i < elements.length; ++i) { + members[i] = elements[i]; + } + const value = new TestValue(content, `${elements[0].typeNames[0]}${space}*`, members); + return value; + } + static fromMembers(typeName: string, members: {[key: string]: TestValue, [key: number]: TestValue}): TestValue { + return new TestValue(new DataView(new ArrayBuffer(0)), typeName, members); + } + + asInt8(): number { + return this.dataView.getInt8(0); + } + asInt16(): number { + return this.dataView.getInt16(0, true); + } + asInt32(): number { + return this.dataView.getInt32(0, true); + } + asInt64(): bigint { + return this.dataView.getBigInt64(0, true); + } + asUint8(): number { + return this.dataView.getUint8(0); + } + asUint16(): number { + return this.dataView.getUint16(0, true); + } + asUint32(): number { + return this.dataView.getUint32(0, true); + } + asUint64(): bigint { + return this.dataView.getBigUint64(0, true); + } + asFloat32(): number { + return this.dataView.getFloat32(0, true); + } + asFloat64(): number { + return this.dataView.getFloat64(0, true); + } + asDataView(offset?: number, size?: number): DataView { + offset = this.location + (offset ?? 0); + size = Math.min(size ?? this.size, this.size - Math.max(0, offset)); + return new DataView(this.dataView.buffer, offset, size); + } + getMembers(): string[] { + return Object.keys(this.members); + } + + $(member: string|number): Value { + if (typeof member === 'number' || !member.includes('.')) { + return this.members[member]; + } + let value = this as Value; + for (const prop of member.split('.')) { + value = value.$(prop); + } + return value; + } + + constructor( + content: DataView, typeName: string, + members?: {[key: string]: TestValue, [key: number]: TestValue}) { + this.location = 0; + this.size = content.byteLength; + this.typeNames = [typeName]; + this.members = members || {}; + this.dataView = content; + } +} + +declare global { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + let __karma__: unknown; +} diff --git a/extensions/cxx_debugging/tests/WasmModule_test.cc b/extensions/cxx_debugging/tests/WasmModule_test.cc new file mode 100644 index 0000000..2c6a7b1 --- /dev/null +++ b/extensions/cxx_debugging/tests/WasmModule_test.cc @@ -0,0 +1,332 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "WasmModule.h" +#include "WasmVendorPlugins.h" + +#include "Plugins/Language/CPlusPlus/CPlusPlusLanguage.h" +#include "Plugins/ObjectFile/wasm/ObjectFileWasm.h" +#include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h" +#include "Plugins/SymbolVendor/wasm/SymbolVendorWasm.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/lldb-types.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/iterator_range.h" +#include "llvm/BinaryFormat/Dwarf.h" + +#include +#include +#include + +namespace symbols_backend { +namespace { + +using LocationRange = std::pair; + +symbols_backend::PluginRegistryContext + c; + +template +auto MakeRange(const ContainerT& c) + -> llvm::iterator_range { + return c; +} + +} // namespace + +TEST(WasmModuleTest, AddScript) { + llvm::Expected> module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/hello.s.wasm"); + ASSERT_TRUE(!!module); + EXPECT_TRUE((*module)->Valid()); +} + +TEST(WasmModuleTest, SourceScripts) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/hello.s.wasm"); + ASSERT_TRUE(!!module); + EXPECT_EQ((*module)->GetSourceScripts().sources.size(), 2u); +} + +TEST(WasmModuleTest, HelloAddScript) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/hello.s.wasm"); + ASSERT_TRUE(!!module); + EXPECT_TRUE((*module)->Valid()); + auto scripts = (*module)->GetSourceScripts(); + llvm::SmallVector filenames; + EXPECT_EQ(scripts.sources.size(), 2u); + EXPECT_EQ(scripts.dwos.size(), 0u); + for (auto& s : scripts.sources) { + filenames.push_back(s); + } + EXPECT_THAT(filenames, testing::UnorderedElementsAre("hello.c", "printf.h")); +} + +TEST(WasmModuleTest, HelloSourceToRawLocation) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/hello.s.wasm"); + ASSERT_TRUE(!!module); + SourceLocation source_location("hello.c", 3, 0); + + auto raw_locations = (*module)->GetOffsetFromSourceLocation(source_location); + EXPECT_THAT(raw_locations, testing::ElementsAre(std::make_pair(2, 3))); +} + +TEST(WasmModuleTest, HelloRawToSourceLocation) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/hello.s.wasm"); + ASSERT_TRUE(!!module); + auto loc = (*module)->GetSourceLocationFromOffset(2, 0); + EXPECT_EQ(loc.size(), 1u); + if (loc.empty()) { + return; + } + const SourceLocation& front_location = *loc.begin(); + EXPECT_EQ(front_location.file, "hello.c"); + EXPECT_EQ(front_location.column, 0u); + EXPECT_EQ(front_location.line, 3u); +} + +TEST(WasmModuleTest, RegressCrbug1153147) { + auto module = WasmModule::CreateFromFile( + "cxx_debugging/tests/inputs/windows_paths.s.wasm"); + ASSERT_TRUE(!!module); + + SourceLocation source_location("c:\\src\\temp.c", 5, 5); + auto raw_locations = (*module)->GetOffsetFromSourceLocation(source_location); + EXPECT_THAT(raw_locations, testing::ElementsAre(std::make_pair(2, 5))); +} + +TEST(WasmModuleTest, InlineSourceToRawLocation) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!module); + SourceLocation source_location("inline.c", 10, 0); + + auto raw_locations = (*module)->GetOffsetFromSourceLocation(source_location); + EXPECT_THAT(raw_locations, + testing::ElementsAre(std::make_pair(lldb::addr_t(0x5), 1))); +} + +TEST(WasmModuleTest, InlineRawToSourceLocation) { + auto m = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!m); + { + auto loc = (*m)->GetSourceLocationFromOffset(0x2, 0); + EXPECT_EQ(loc.size(), 1u); + if (loc.empty()) { + return; + } + const SourceLocation& front_location = *loc.begin(); + EXPECT_EQ(front_location.file, "inline.c"); + EXPECT_EQ(front_location.column, 0u); + EXPECT_EQ(front_location.line, 1u); + } + { + auto loc = (*m)->GetSourceLocationFromOffset(0x5, 0); + EXPECT_EQ(loc.size(), 1u); + if (loc.empty()) { + return; + } + const SourceLocation& front_location = *loc.begin(); + EXPECT_EQ(front_location.file, "inline.c"); + EXPECT_EQ(front_location.column, 0u); + EXPECT_EQ(front_location.line, 10u); + } + { + auto loc = (*m)->GetSourceLocationFromOffset(0x6, 0); + EXPECT_EQ(loc.size(), 1u); + if (loc.empty()) { + return; + } + const SourceLocation& front_location = *loc.begin(); + EXPECT_EQ(front_location.file, "inline.c"); + EXPECT_EQ(front_location.column, 0u); + EXPECT_EQ(front_location.line, 2u); + } +} + +TEST(WasmModuleTest, InlineFunctionInfo) { + auto m = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!m); + { + auto function_names = (*m)->GetFunctionInfo(0x2).names; + EXPECT_THAT(function_names, testing::ElementsAre("caller")); + } + { + auto function_names = (*m)->GetFunctionInfo(0x5).names; + EXPECT_THAT(function_names, testing::ElementsAre("callee", "caller")); + } +} + +TEST(WasmModuleTest, InlineFunctionRanges) { + auto m = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!m); + { + llvm::SmallVector function_ranges( + MakeRange((*m)->GetInlineFunctionAddressRanges(0x5))); + EXPECT_THAT(function_ranges.size(), 1u); + EXPECT_THAT(function_ranges, testing::ElementsAre(std::make_pair(0x5, 1))); + } +} + +TEST(WasmModuleTest, ChildInlineFunctionRanges) { + auto m = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!m); + llvm::SmallVector function_ranges( + MakeRange((*m)->GetChildInlineFunctionAddressRanges(0x2))); + EXPECT_THAT(function_ranges.size(), 1u); + EXPECT_THAT(function_ranges, testing::ElementsAre(std::make_pair(0x5, 1))); +} + +TEST(WasmModuleTest, AddScriptMissingScript) { + auto m = WasmModule::CreateFromFile("@InvalidPath"); + EXPECT_FALSE(m); + llvm::consumeError(m.takeError()); +} + +TEST(WasmModuleTest, GlobalVariable) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/globals.s.wasm"); + ASSERT_TRUE(!!module); + auto variables = (*module)->GetVariablesInScope(0x3, 0); + std::vector names; + names.reserve(variables.size()); + for (auto& v : variables) { + names.push_back(v.name); + } + llvm::sort(names); + EXPECT_THAT(names, testing::UnorderedElementsAreArray({ + "i", + "S::buffer", + "::i_ptr", + "::v_ptr", + "::var_bool", + "::var_char", + "::var_double", + "::var_float", + "::var_int", + "::var_int16_t", + "::var_int32_t", + "::var_int64_t", + "::var_int8_t", + "::var_long", + "::var_long_int", + "::var_long_long", + "::var_long_long_int", + "::var_short", + "::var_short_int", + "::var_signed", + "::var_signed_char", + "::var_signed_int", + "::var_signed_long", + "::var_signed_long_int", + "::var_signed_long_long", + "::var_signed_long_long_int", + "::var_signed_short", + "::var_signed_short_int", + "::var_uint16_t", + "::var_uint32_t", + "::var_uint64_t", + "::var_uint8_t", + "::var_unsigned", + "::var_unsigned_char", + "::var_unsigned_int", + "::var_unsigned_long", + "::var_unsigned_long_int", + "::var_unsigned_long_long", + "::var_unsigned_long_long_int", + "::var_unsigned_short", + "::var_unsigned_short_int", + "::var_int128", + "::var_uint128", + })); +} + +TEST(WasmModuleTest, ClassStaticVariable) { + auto module = WasmModule::CreateFromFile( + "cxx_debugging/tests/inputs/classstatic.s.wasm"); + ASSERT_TRUE(!!module); + auto variables = (*module)->GetVariablesInScope(0x3, 0); + llvm::SmallVector names; + for (auto& v : variables) { + names.push_back(v.name); + } + EXPECT_THAT(names, testing::UnorderedElementsAre("MyClass::I")); +} + +TEST(WasmModuleTest, NamespacedGlobalVariables) { + auto m = WasmModule::CreateFromFile( + "cxx_debugging/tests/inputs/namespaces.s.wasm"); + ASSERT_TRUE(!!m); + auto variables = (*m)->GetVariablesInScope(0x03, 0); + llvm::SmallVector names; + for (auto& v : variables) { + names.push_back(v.name); + } + EXPECT_THAT(names, testing::UnorderedElementsAre("L", "n1::n2::I", "n1::I", + "n1::MyClass::I", + "(anonymous namespace)::K")); +} + +TEST(WasmModuleTest, InlineLocalVariable) { + auto m = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/inline.s.wasm"); + ASSERT_TRUE(!!m); + { + auto variables = (*m)->GetVariablesInScope(0x2, 0); + llvm::SmallVector names; + for (auto& v : variables) { + names.push_back(v.name); + } + EXPECT_THAT(names, testing::UnorderedElementsAre("outer_var")); + } + { + auto variables = (*m)->GetVariablesInScope(0x5, 0); + llvm::SmallVector names; + for (auto& v : variables) { + names.push_back(v.name); + } + EXPECT_THAT(names, + testing::UnorderedElementsAre("inner_var", "inner_param")); + } +} + +TEST(WasmModuleTest, ShadowingVariables) { + auto module = + WasmModule::CreateFromFile("cxx_debugging/tests/inputs/shadowing.s.wasm"); + ASSERT_TRUE(!!module); + + auto variables = (*module)->GetVariablesInScope(0x04, 0); + ASSERT_EQ(variables.size(), 1); + + auto var = variables.begin(); + ASSERT_EQ(var->name, "a"); + ASSERT_EQ(var->scope, lldb::eValueTypeVariableLocal); + + variables = (*module)->GetVariablesInScope(0x02, 0); + ASSERT_EQ(variables.size(), 1); + + var = variables.begin(); + ASSERT_EQ(var->name, "a"); + ASSERT_EQ(var->scope, lldb::eValueTypeVariableArgument); +} + +} // namespace symbols_backend diff --git a/extensions/cxx_debugging/tests/inputs/CMakeLists.txt b/extensions/cxx_debugging/tests/inputs/CMakeLists.txt new file mode 100644 index 0000000..eb02a79 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/CMakeLists.txt @@ -0,0 +1,141 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +get_filename_component(llvm_bin ${LLVM_TABLEGEN} DIRECTORY) +set(LLVM_MC ${llvm_bin}/llvm-mc) + +set(EOM ".int8 0") +set(ABBR ".int8") +file(STRINGS dw_opcodes.def DW_OPCODES) +list(APPEND CMAKE_CONFIGURE_DEPENDS dw_opcodes.def) +foreach(opcode_def IN ITEMS ${DW_OPCODES}) + if (opcode_def MATCHES "^[ \t]*#" OR opcode_def MATCHES "^[ \]*$") + continue() + endif() + string(REGEX REPLACE "[ \t]+" ";" split ${opcode_def}) + list(GET split 0 opcode) + list(GET split 1 value) + set(${opcode} ".int8 ${value} # ${opcode}") +endforeach() + +set(compiled_inputs) +macro(add_test_inputs) + cmake_parse_arguments(option "DWO" "" "" ${ARGN}) + foreach(input IN ITEMS ${option_UNPARSED_ARGUMENTS}) + configure_file(${input} ${input}.preprocessed) + set(input_file ${CMAKE_CURRENT_BINARY_DIR}/${input}.preprocessed) + set(mc_args ${input_file} -triple wasm32-unknown-unknown -filetype=obj -o ${input}.wasm) + if (option_DWO) + add_custom_command(OUTPUT ${input}.wasm ${input}.dwo + DEPENDS ${input_file} + COMMAND ${LLVM_MC} ${mc_args} -split-dwarf-file=${input}.dwo + ) + list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/${input}.wasm ${CMAKE_CURRENT_BINARY_DIR}/${input}.dwo) + else() + add_custom_command(OUTPUT ${input}.wasm + DEPENDS ${input_file} + COMMAND ${LLVM_MC} ${mc_args} + ) + list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/${input}.wasm) + endif() + endforeach() +endmacro() + +macro(add_dwo_test_inputs) + set(obj_inputs) + cmake_parse_arguments(option "" "DESTINATION" "" ${ARGN}) + + foreach(input IN ITEMS ${option_UNPARSED_ARGUMENTS}) + get_filename_component(filename ${input} NAME_WE) + configure_file(${input} ${filename}.preprocessed) + + set(input_file ${CMAKE_CURRENT_BINARY_DIR}/${filename}.preprocessed) + set(mc_args ${input_file} -triple wasm32-unknown-unknown -g -filetype=obj -o ${filename}.o) + add_custom_command(OUTPUT ${filename}.o ${filename}.dwo + DEPENDS ${input_file} + COMMAND ${LLVM_MC} ${mc_args} --split-dwarf-file=${filename}.dwo + ) + list(APPEND obj_inputs ${CMAKE_CURRENT_BINARY_DIR}/${filename}.o) + list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/${filename}.dwo ${CMAKE_CURRENT_BINARY_DIR}/${filename}.o) + endforeach() + add_custom_command(OUTPUT ${option_DESTINATION} + DEPENDS ${obj_inputs} + COMMAND ${WASM_LD} --export-all --no-entry -o ${option_DESTINATION} ${obj_inputs}) + list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/${option_DESTINATION}) +endmacro() + + +set(compiled_wasm_inputs) +macro(add_test_program program) + add_executable(${program} ${ARGN}) + set(compile_options -g -gdwarf-5 -O0) + set(link_options -g -gdwarf-5 -gseparate-dwarf + -O0 + -sERROR_ON_WASM_CHANGES_AFTER_LINK=1 + -sMODULARIZE=1 + -sWASM_BIGINT + -sEXPORT_NAME=loadModule + -sEXPORT_ES6=1 + -sEXPORT_ALL=1 + -std=c++17 + -Wl,--export-all + -Wl,--no-gc-sections + ) + # Set link and compile options directly to avoid accidentally inheriting + # global flags. + set_target_properties(${program} PROPERTIES + COMPILE_OPTIONS "${compile_options}" + LINK_OPTIONS "${link_options}") + list(APPEND compiled_inputs + ${CMAKE_CURRENT_BINARY_DIR}/${program}.js + ) + list(APPEND compiled_wasm_inputs + ${CMAKE_CURRENT_BINARY_DIR}/${program}.wasm + ${CMAKE_CURRENT_BINARY_DIR}/${program}.wasm.debug.wasm + ) +endmacro() + +add_test_inputs(DWO split-dwarf.s) +add_test_inputs( + embedded.s + addr_index.s + enums.s + hello.s + windows_paths.s + globals.s + classstatic.s + namespaces.s + shadowing.s + inline.s + externref.s +) + +add_dwo_test_inputs(DESTINATION hello-split.wasm helper.s hello-split.s) +add_dwo_test_inputs(DESTINATION hello-split-missing-dwo.wasm hello-split-missing-dwo.s) +# Explicitly remove dwo files for tests with missing dwos +set(missing_dwos "hello-split-missing-dwo.dwo") + +add_test_program(addresses_main addresses.cc) +add_test_program(lldb_eval_inputs + ${THIRD_PARTY_DIR}/lldb-eval/src/testdata/test_binary.cc + ${THIRD_PARTY_DIR}/lldb-eval/src/testdata/test_library.cc) + + +set(TEST_BINARY_INPUTS + page.html + page.js + externref.js + ) + +set(binary_inputs) +foreach(input IN LISTS TEST_BINARY_INPUTS) + list(APPEND binary_inputs ${CMAKE_CURRENT_BINARY_DIR}/${input}) + add_custom_command(OUTPUT ${input} DEPENDS ${input} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${input} ${CMAKE_CURRENT_BINARY_DIR}/) +endforeach() +add_custom_target(SymbolsBackendTestInputs DEPENDS ${compiled_inputs} ${TEST_BINARY_INPUTS}) +add_custom_command(TARGET SymbolsBackendTestInputs POST_BUILD COMMAND rm ${missing_dwos}) + +list(APPEND EXTENSION_TEST_BUILD_ARTIFACTS ${compiled_inputs} ${binary_inputs} ${compiled_wasm_inputs}) +set(EXTENSION_TEST_BUILD_ARTIFACTS ${EXTENSION_TEST_BUILD_ARTIFACTS} PARENT_SCOPE) diff --git a/extensions/cxx_debugging/tests/inputs/addr_index.s b/extensions/cxx_debugging/tests/inputs/addr_index.s new file mode 100644 index 0000000..5db837a --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/addr_index.s @@ -0,0 +1,97 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + # Need a non-empty code section for variable lookups to be able to find the + # comp unit by code offset. + .file "embedded.c" + .globl __original_main + .type __original_main,@function +__original_main: # @__original_main + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.Lfunc_end0: + .size __original_main, .Lfunc_end0-__original_main + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + .int8 1 # Abbrev [1] DW_TAG_compile_unit + .int32 0x8 # DW_AT_low_pc + .int32 0x10 # DW_AT_high_pc + .int32 .Lstring0 # DW_AT_comp_dir + .int32 .debug_addr # DW_AT_GNU_addr_base + + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lstring1 # DW_AT_name + .int32 .Ldebug_info_type0 # DW_AT_type + .int8 2 # DW_AT_LOCATION + .int8 0xfb # DW_OP_GNU_addr_index + .int8 0 + +.Ldebug_info_type0: + .int8 3 # Abbrev [3] DW_TAG_type + .int32 .Lstring2 # DW_AT_name + .int8 0x07 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + ${EOM} +.Ldebug_info_end0: + + .section .debug_addr,"",@ +.Laddr_table_base0: + .int32 0xffffffff + + .section .debug_str,"S",@ +.Lstring0: + .asciz "tests/inputs" +.Lstring1: + .asciz "optimized_out" +.Lstring2: + .asciz "uint32_t" + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + .int8 6 # DW_FORM_data4 + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .ascii "\263B" # DW_AT_GNU_addr_base + .int8 23 # DW_FORM_sec_offset + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 2 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 2 # DW_AT_location + .int8 24 # DW_FORM_exprloc + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 3 # Abbreviation Code + .int8 0x24 # DW_TAG_base_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x3e # DW_AT_encoding + .int8 0x0b # DW_FORM_data1 + .int8 0x0b # DW_AT_byte_size + .int8 0x0b # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 0 # EOM(3) diff --git a/extensions/cxx_debugging/tests/inputs/addresses.cc b/extensions/cxx_debugging/tests/inputs/addresses.cc new file mode 100644 index 0000000..a07a481 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/addresses.cc @@ -0,0 +1,25 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +void calcSum(int* x, int length, int& sum) { + for (int i = 0; i < length; ++i) { + sum += x[i]; + } +} + +int main() { + int sum = 0, n = 10; + int x[10]; + + /* initialize x */ + for (int i = 0; i < 10; ++i) { + x[i] = n - i - 1; + } + + calcSum(x, n, sum); + // BREAK(ArrayMembersTest) + std::cerr << sum << "\n"; +} diff --git a/extensions/cxx_debugging/tests/inputs/classstatic.s b/extensions/cxx_debugging/tests/inputs/classstatic.s new file mode 100644 index 0000000..e5f8d08 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/classstatic.s @@ -0,0 +1,119 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.L__original_main_end: + + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + .int8 1 # Abbrev [1] DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lcomp_dir # DW_AT_comp_dir + +.Ltype: + ${ABBR} 2 # DW_TAG_type + .int32 .Ltype_name # DW_AT_name + .int8 0x07 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + + ${ABBR} 3 # DW_TAG_variable + .int32 .Llinkage_name # DW_AT_linkage_name + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int32 .Ldeclaration # DW_AT_specification + + ${ABBR} 4 # DW_TAG_class_type + .int32 .Lclass_name # DW_AT_name + +.Ldeclaration: + ${ABBR} 5 # DW_TAG_member + .int32 .Lmember_name # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 1 # DW_AT_external + .int8 1 # DW_AT_declaration + ${EOM} # End DW_TAG_class_type + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end0: + + .section .debug_str,"S",@ +.Lcomp_dir: + .asciz "/tmp/" +.Ltype_name: + .asciz "int" +.Llinkage_name: + .asciz "_ZN7MyClass1IE" +.Lclass_name: + .asciz "MyClass" +.Lmember_name: + .asciz "I" + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + .int8 6 # DW_FORM_data4 + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 2 # Abbreviation Code + .int8 0x24 # DW_TAG_base_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x3e # DW_AT_encoding + .int8 0x0b # DW_FORM_data1 + .int8 0x0b # DW_AT_byte_size + .int8 0x0b # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 3 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 0x6e # DW_AT_linkage_name + .int8 14 # DW_FORM_strp + .int8 2 # DW_AT_location + .int8 24 # DW_FORM_exprloc + .int8 0x47 # DW_AT_specification + .int8 0x10 # DW_FORM_ref_addr + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 4 # Abbreviation Code + .int8 2 # DW_TAG_class_type + .int8 1 # DW_CHILDREN_yes + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 5 # Abbreviation Code + .int8 0x0d # DW_TAG_member + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 0x3f # DW_AT_external + .int8 0x0c # DW_FORM_flag + .int8 0x3c # DW_AT_declaration + .int8 0x0c # DW_FORM_flag + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) diff --git a/extensions/cxx_debugging/tests/inputs/dw_opcodes.def b/extensions/cxx_debugging/tests/inputs/dw_opcodes.def new file mode 100644 index 0000000..aff3703 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/dw_opcodes.def @@ -0,0 +1,66 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Attributes +DW_AT_abstract_origin 0x31 +DW_AT_comp_dir 0x1b +DW_AT_declaration 0x3c +DW_AT_external 0x3f +DW_AT_high_pc 0x12 +DW_AT_inline 0x20 +DW_AT_linkage_name 0x6e +DW_AT_location 0x02 +DW_AT_low_pc 0x11 +DW_AT_name 0x03 +DW_AT_specification 0x47 +DW_AT_stmt_list 0x10 +DW_AT_type 0x49 +DW_AT_producer 0x25 +DW_AT_language 0x13 +DW_AT_addr_base 0x73 +DW_AT_dwo_name 0x76 +DW_AT_str_offsets_base 0x72 +DW_AT_frame_base 0x40 +DW_AT_decl_file 0x3a +DW_AT_decl_line 0x3b +DW_AT_prototyped 0x27 +DW_CHILDREN_no 0x00 +DW_CHILDREN_yes 0x01 + +# Forms +DW_FORM_addr 0x01 +DW_FORM_addrx 0x1b +DW_FORM_data1 0x0b +DW_FORM_data4 0x06 +DW_FORM_exprloc 0x18 +DW_FORM_flag 0x0c +DW_FORM_flag_present 0x19 +DW_FORM_ref_addr 0x10 +DW_FORM_ref4 0x13 +DW_FORM_sec_offset 0x17 +DW_FORM_strp 0x0e +DW_FORM_strx1 0x25 +DW_FORM_data2 0x05 +DW_INL_inlined 0x01 + +# Opcodes +DW_OP_WASM_location 0xed +DW_OP_addr 0x03 +DW_OP_fbreg 0x91 +DW_OP_stack_value 0x9f + +# TAGS +DW_TAG_base_type 0x24 +DW_TAG_class_type 0x02 +DW_TAG_skeleton_unit 0x4a +DW_TAG_compile_unit 0x11 +DW_TAG_formal_parameter 0x05 +DW_TAG_inlined_subroutine 0x1d +DW_TAG_lexical_block 0x0b +DW_TAG_member 0x0d +DW_TAG_namespace 0x39 +DW_TAG_subprogram 0x2e +DW_TAG_variable 0x34 +DW_TAG_structure_type 0x13 +DW_TAG_typedef 0x16 diff --git a/extensions/cxx_debugging/tests/inputs/embedded.s b/extensions/cxx_debugging/tests/inputs/embedded.s new file mode 100644 index 0000000..253bac9 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/embedded.s @@ -0,0 +1,124 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .file "embedded.c" + .globl __original_main + .type __original_main,@function +__original_main: # @__original_main + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.Lfunc_end0: + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + + ${ABBR} 1 # DW_TAG_compile_unit + .int32 0x8 # DW_AT_low_pc + .int32 0x10 # DW_AT_high_pc + .int32 .Lstring0 # DW_AT_comp_dir + + ${ABBR} 2 # DW_TAG_variable + .int32 0xDEADBEEF # DW_AT_const_value + .int32 .Lstring1 # DW_AT_name + .int32 .Ldebug_info_type0 # DW_AT_type + + ${ABBR} 5 # DW_TAG_variable + .int32 0xDEADBEEF + .int32 .Lstring3 # DW_AT_name + .int32 .Ldebug_info_type1 # DW_AT_type + +.Ldebug_info_type0: + ${ABBR} 3 # DW_TAG_type + .int32 .Lstring2 # DW_AT_name + .int8 0x07 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + +.Ldebug_info_type1: + ${ABBR} 4 # DW_TAG_pointer_type + .int32 .Lstring4 # DW_AT_name + .int8 4 # DW_AT_byte_size + .int32 .Ldebug_info_type0 # DW_AT_type + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end0: + + .section .debug_addr,"",@ +.Laddr_table_base0: + .int32 0xbeef + + .section .debug_str,"S",@ +.Lstring0: + .asciz "tests/inputs" +.Lstring1: + .asciz "constant_var" +.Lstring2: + .asciz "uint32_t" +.Lstring3: + .asciz "constant_var_ptr" +.Lstring4: + .asciz "uint32_t *" + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + .int8 6 # DW_FORM_data4 + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 2 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 0x1c # DW_AT_const_value + .int8 6 # DW_FORM_data4 + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 3 # Abbreviation Code + .int8 0x24 # DW_TAG_base_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x3e # DW_AT_encoding + .int8 0x0b # DW_FORM_data1 + .int8 0x0b # DW_AT_byte_size + .int8 0x0b # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 4 # Abbreviateion code + .int8 0x0f # DW_TAG_pointer_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x0b # DW_AT_byte_size + .int8 0x0b # DW_FORM_data1 + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 5 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 0x1c # DW_AT_const_value + .int8 6 # DW_FORM_data4 + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) diff --git a/extensions/cxx_debugging/tests/inputs/enums.s b/extensions/cxx_debugging/tests/inputs/enums.s new file mode 100644 index 0000000..11f60da --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/enums.s @@ -0,0 +1,376 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + # Need a non-empty code section for variable lookups to be able to find the + # comp unit by code offset. + .file "embedded.c" + .globl __original_main + .type __original_main,@function +__original_main: # @__original_main + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.Lfunc_end0: + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + .int8 6 # DW_FORM_data4 + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 2 # Abbreviation Code + .int8 4 # DW_TAG_enumeration_type + .int8 1 # DW_CHILDREN_yes + .int8 73 # DW_AT_type + .int8 19 # DW_FORM_ref4 + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 11 # DW_AT_byte_size + .int8 11 # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 3 # Abbreviation Code + .int8 40 # DW_TAG_enumerator + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 28 # DW_AT_const_value + .int8 15 # DW_FORM_udata + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 4 # Abbreviation Code + .int8 36 # DW_TAG_base_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 62 # DW_AT_encoding + .int8 11 # DW_FORM_data1 + .int8 11 # DW_AT_byte_size + .int8 11 # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 5 # Abbreviation Code + .int8 4 # DW_TAG_enumeration_type + .int8 1 # DW_CHILDREN_yes + .int8 73 # DW_AT_type + .int8 19 # DW_FORM_ref4 + .int8 109 # DW_AT_enum_class + .int8 25 # DW_FORM_flag_present + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 11 # DW_AT_byte_size + .int8 11 # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 6 # Abbreviation Code + .int8 40 # DW_TAG_enumerator + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 28 # DW_AT_const_value + .int8 13 # DW_FORM_sdata + .int8 0 # EOM(1) + .int8 0 # EOM(2) + + .int8 7 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 2 # DW_AT_location + .int8 24 # DW_FORM_exprloc + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 5 # DWARF version number + .int8 1 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 .debug_abbrev # Offset Into Abbrev. Section + .int8 1 # Abbrev [1] DW_TAG_compile_unit + # Hard coded value here to address the CU in the test + .int32 0x8 # DW_AT_low_pc + .int32 0x32 # DW_AT_high_pc + .int32 .Lstring_comp_dir # DW_AT_comp_dir + +.Ltype_int32: + .int8 4 # Abbrev [4] DW_TAG_base_type + .int32 .Lstring_int32 # DW_AT_name + .int8 5 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + +.Ltype_uint32: + .int8 4 # Abbrev [4] DW_TAG_base_type + .int32 .Lstring_int64 # DW_AT_name + .int8 5 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + +.Ltype_int64: + .int8 4 # Abbrev [4] DW_TAG_base_type + .int32 .Lstring_uint32 # DW_AT_name + .int8 7 # DW_AT_encoding + .int8 8 # DW_AT_byte_size + +.Ltype_uint64: + .int8 4 # Abbrev [4] DW_TAG_base_type + .int32 .Lstring_uint64 # DW_AT_name + .int8 7 # DW_AT_encoding + .int8 8 # DW_AT_byte_size + +.Ltype_enum_int32: + .int8 2 # Abbrev [2] DW_TAG_enumeration_type + .int32 .Ltype_int32 # DW_AT_type + .int32 .Lstring_enum_i32 # DW_AT_name + .int8 4 # DW_AT_byte_size + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_i32_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_i32_enum1 # DW_AT_name + .int8 0x01 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_uint32: + .int8 2 # Abbrev [2] DW_TAG_enumeration_type + .int32 .Ltype_uint32 # DW_AT_type + .int32 .Lstring_enum_u32 # DW_AT_name + .int8 4 # DW_AT_byte_size + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_u32_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_u32_enum1 # DW_AT_name + .int8 0x04 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_int64: + .int8 2 # Abbrev [2] DW_TAG_enumeration_type + .int32 .Ltype_int64 # DW_AT_type + .int32 .Lstring_enum_i64 # DW_AT_name + .int8 8 # DW_AT_byte_size + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_i64_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_i64_enum1 # DW_AT_name + .int8 0x08 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_uint64: + .int8 2 # Abbrev [2] DW_TAG_enumeration_type + .int32 .Ltype_uint64 # DW_AT_type + .int32 .Lstring_enum_u64 # DW_AT_name + .int8 8 # DW_AT_byte_size + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_u64_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 3 # Abbrev [3] DW_TAG_enumerator + .int32 .Lstring_enum_u64_enum1 # DW_AT_name + .int8 0x10 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_class_int32: + .int8 5 # Abbrev [5] DW_TAG_enumeration_type + .int32 .Ltype_int32 # DW_AT_type + .int32 .Lstring_enum_class_i32 # DW_AT_name + .int8 4 # DW_AT_byte_size + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_i32_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_i32_enum1 # DW_AT_name + .int8 0x18 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_class_uint32: + .int8 5 # Abbrev [5] DW_TAG_enumeration_type + .int32 .Ltype_uint32 # DW_AT_type + .int32 .Lstring_enum_class_u32 # DW_AT_name + .int8 4 # DW_AT_byte_size + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_u32_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_u32_enum1 # DW_AT_name + .int8 0x1C # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_class_int64: + .int8 5 # Abbrev [5] DW_TAG_enumeration_type + .int32 .Ltype_int64 # DW_AT_type + .int32 .Lstring_enum_class_i64 # DW_AT_name + .int8 8 # DW_AT_byte_size + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_i64_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_i64_enum1 # DW_AT_name + .int8 0x20 # DW_AT_const_value + .int8 0 # End Of Children Mark + +.Ltype_enum_class_uint64: + .int8 5 # Abbrev [5] DW_TAG_enumeration_type + .int32 .Ltype_uint64 # DW_AT_type + .int32 .Lstring_enum_class_u64 # DW_AT_name + .int8 8 # DW_AT_byte_size + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_u64_enum0 # DW_AT_name + .int8 0 # DW_AT_const_value + .int8 6 # Abbrev [6] DW_TAG_enumerator + .int32 .Lstring_enum_class_u64_enum1 # DW_AT_name + .int8 0x28 # DW_AT_const_value + .int8 0 # End Of Children Mark + + .int8 7 # Abbrev [7] DW_TAG_variable: + .int32 .Lstring_var_enum_int32 # DW_AT_name + .int32 .Ltype_enum_int32 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_uint32 # DW_AT_name + .int32 .Ltype_enum_uint32 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x04 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_int64 # DW_AT_name + .int32 .Ltype_enum_int64 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x08 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_uint64 # DW_AT_name + .int32 .Ltype_enum_uint64 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x10 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_class_int32 # DW_AT_name + .int32 .Ltype_enum_class_int32 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x18 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_class_uint32 # DW_AT_name + .int32 .Ltype_enum_class_uint32 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x1C + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_class_int64 # DW_AT_name + .int32 .Ltype_enum_class_int64 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x20 + + .int8 7 # Abbrev [7] DW_TAG_variable + .int32 .Lstring_var_enum_class_uint64 # DW_AT_name + .int32 .Ltype_enum_class_uint64 # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x28 + + ${EOM} # End DW_AT_compile_unit +.Ldebug_info_end0: + + .section .debug_str,"S",@ +.Lstring_comp_dir: + .asciz "/tmp" +.Lstring_int32: + .asciz "int32_t" +.Lstring_int64: + .asciz "int64_t" +.Lstring_uint32: + .asciz "uint32_t" +.Lstring_uint64: + .asciz "uint64_t" +.Lstring_enum_i32: + .asciz "enum_i32" +.Lstring_enum_i32_enum0: + .asciz "enum_i32_enum0" +.Lstring_enum_i32_enum1: + .asciz "enum_i32_enum1" +.Lstring_enum_u32: + .asciz "enum_u32" +.Lstring_enum_u32_enum0: + .asciz "enum_u32_enum0" +.Lstring_enum_u32_enum1: + .asciz "enum_u32_enum1" +.Lstring_enum_i64: + .asciz "enum_i64" +.Lstring_enum_i64_enum0: + .asciz "enum_i64_enum0" +.Lstring_enum_i64_enum1: + .asciz "enum_i64_enum1" +.Lstring_enum_u64: + .asciz "enum_u64" +.Lstring_enum_u64_enum0: + .asciz "enum_u64_enum0" +.Lstring_enum_u64_enum1: + .asciz "enum_u64_enum1" +.Lstring_enum_class_i32: + .asciz "enum_class_i32" +.Lstring_enum_class_i32_enum0: + .asciz "enum_class_i32_enum0" +.Lstring_enum_class_i32_enum1: + .asciz "enum_class_i32_enum1" +.Lstring_enum_class_u32: + .asciz "enum_class_u32" +.Lstring_enum_class_u32_enum0: + .asciz "enum_class_u32_enum0" +.Lstring_enum_class_u32_enum1: + .asciz "enum_class_u32_enum1" +.Lstring_enum_class_i64: + .asciz "enum_class_i64" +.Lstring_enum_class_i64_enum0: + .asciz "enum_class_i64_enum0" +.Lstring_enum_class_i64_enum1: + .asciz "enum_class_i64_enum1" +.Lstring_enum_class_u64: + .asciz "enum_class_u64" +.Lstring_enum_class_u64_enum0: + .asciz "enum_class_u64_enum0" +.Lstring_enum_class_u64_enum1: + .asciz "enum_class_u64_enum1" +.Lstring_var_enum_int32: + .asciz "var_enum_i32" +.Lstring_var_enum_uint32: + .asciz "var_enum_u32" +.Lstring_var_enum_int64: + .asciz "var_enum_i64" +.Lstring_var_enum_uint64: + .asciz "var_enum_u64" +.Lstring_var_enum_class_int32: + .asciz "var_enum_class_i32" +.Lstring_var_enum_class_uint32: + .asciz "var_enum_class_u32" +.Lstring_var_enum_class_int64: + .asciz "var_enum_class_i64" +.Lstring_var_enum_class_uint64: + .asciz "var_enum_class_u64" diff --git a/extensions/cxx_debugging/tests/inputs/externref.js b/extensions/cxx_debugging/tests/inputs/externref.js new file mode 100644 index 0000000..cc82248 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/externref.js @@ -0,0 +1,22 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +async function f() { + const code = fetch('externref.s.wasm'); + const imports = { + env: { + __linear_memory: new WebAssembly.Memory({initial: 0}), + __indirect_function_table: new WebAssembly.Table({ + element: 'anyfunc', + initial: 0, + }), + }, + }; + const wasmModule = await WebAssembly.instantiateStreaming(code, imports); + return { + _main: () => wasmModule.instance.exports.f({x: 1, y: 'titi'}, 'test'), + }; +} +// eslint-disable-next-line import/no-default-export +export default f; diff --git a/extensions/cxx_debugging/tests/inputs/externref.s b/extensions/cxx_debugging/tests/inputs/externref.s new file mode 100644 index 0000000..de575c4 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/externref.s @@ -0,0 +1,207 @@ + .text + .file "externref.c" + .functype f (externref, externref) -> (externref) + .export_name f, f + .section .text.f,"",@ + .hidden f # -- Begin function f + .globl f + .type f,@function +f: # @f +.Lfunc_begin0: + .functype f (externref, externref) -> (externref) +# %bb.0: + #DEBUG_VALUE: f:x <- !target-index(0,0) + #DEBUG_VALUE: f:y <- !target-index(0,1) + .file 1 "/tmp" "externref.c" + .loc 1 3 3 prologue_end # externref.c:3:3 + local.get 0 + return + end_function +.Ltmp0: +.Lfunc_end0: + # -- End function + .no_dead_strip f + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_producer} + ${DW_FORM_strp} + ${DW_AT_language} + ${DW_FORM_data2} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${EOM} + ${EOM} + ${ABBR} 2 + ${DW_TAG_subprogram} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${DW_AT_frame_base} + ${DW_FORM_exprloc} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_decl_file} + ${DW_FORM_data1} + ${DW_AT_decl_line} + ${DW_FORM_data1} + ${DW_AT_prototyped} + ${DW_FORM_flag_present} + ${DW_AT_type} + ${DW_FORM_ref4} + ${DW_AT_external} + ${DW_FORM_flag_present} + ${EOM} + ${EOM} + ${ABBR} 3 + ${DW_TAG_formal_parameter} + ${DW_CHILDREN_no} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_decl_file} + ${DW_FORM_data1} + ${DW_AT_decl_line} + ${DW_FORM_data1} + ${DW_AT_type} + ${DW_FORM_ref4} + ${EOM} + ${EOM} + ${ABBR} 4 + ${DW_TAG_typedef} + ${DW_CHILDREN_no} + ${DW_AT_type} + ${DW_FORM_ref4} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 5 + ${DW_TAG_structure_type} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_declaration} + ${DW_FORM_flag_present} + ${EOM} + ${EOM} + ${EOM} + .section .debug_info,"",@ +.Lcu_begin0: + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 .debug_abbrev0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + ${ABBR} 1 # Abbrev [1] 0xb:0x66 DW_TAG_compile_unit + .int32 .Linfo_string0 # DW_AT_producer + .int16 29 # DW_AT_language + .int32 .Linfo_string1 # DW_AT_name + .int32 .Lline_table_start0 # DW_AT_stmt_list + .int32 .Linfo_string2 # DW_AT_comp_dir + .int32 .Lfunc_begin0 # DW_AT_low_pc + .int32 .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc + ${ABBR} 2 # Abbrev [2] 0x26:0x3c DW_TAG_subprogram + .int32 .Lfunc_begin0 # DW_AT_low_pc + .int32 .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc + .int8 7 # DW_AT_frame_base + .int8 237 + .int8 3 + .int32 __stack_pointer + .int8 159 + .int32 .Linfo_string3 # DW_AT_name + .int8 1 # DW_AT_decl_file + .int8 2 # DW_AT_decl_line + # DW_AT_prototyped + .int32 98 # DW_AT_type + # DW_AT_external + ${ABBR} 3 # Abbrev [3] 0x41:0x10 DW_TAG_formal_parameter + .int8 4 # DW_AT_location + .int8 237 + .int8 0 + .int8 0 + .int8 159 + .int32 .Linfo_string6 # DW_AT_name + .int8 1 # DW_AT_decl_file + .int8 2 # DW_AT_decl_line + .int32 98 # DW_AT_type + ${ABBR} 3 # Abbrev [3] 0x51:0x10 DW_TAG_formal_parameter + .int8 4 # DW_AT_location + .int8 237 + .int8 0 + .int8 1 + .int8 159 + .int32 .Linfo_string7 # DW_AT_name + .int8 1 # DW_AT_decl_file + .int8 2 # DW_AT_decl_line + .int32 98 # DW_AT_type + ${EOM} # End Of Children Mark + ${ABBR} 4 # Abbrev [4] 0x62:0x9 DW_TAG_typedef + .int32 107 # DW_AT_type + .int32 .Linfo_string5 # DW_AT_name + ${ABBR} 5 # Abbrev [5] 0x6b:0x5 DW_TAG_structure_type + .int32 .Linfo_string4 # DW_AT_name + # DW_AT_declaration + ${EOM} # End Of Children Mark +.Ldebug_info_end0: + .section .debug_str,"S",@ +.Linfo_string0: + .asciz "Ubuntu clang version 17.0.6 (++20240124120726+6009708b4367-1~exp1~20240124120743.47)" # string offset=0 +.Linfo_string1: + .asciz "externref.c" # string offset=85 +.Linfo_string2: + .asciz "/tmp" # string offset=97 +.Linfo_string3: + .asciz "f" # string offset=102 +.Linfo_string4: + .asciz "externref_t" # string offset=104 +.Linfo_string5: + .asciz "__externref_t" # string offset=116 +.Linfo_string6: + .asciz "x" # string offset=130 +.Linfo_string7: + .asciz "y" # string offset=132 + .ident "Ubuntu clang version 17.0.6 (++20240124120726+6009708b4367-1~exp1~20240124120743.47)" + .no_dead_strip __indirect_function_table + .section .custom_section.producers,"",@ + .int8 2 + .int8 8 + .ascii "language" + .int8 1 + .int8 3 + .ascii "C11" + .int8 0 + .int8 12 + .ascii "processed-by" + .int8 1 + .int8 12 + .ascii "Ubuntu clang" + .int8 63 + .ascii "17.0.6 (++20240124120726+6009708b4367-1~exp1~20240124120743.47)" + .section .debug_str,"S",@ + .section .custom_section.target_features,"",@ + .int8 3 + .int8 43 + .int8 15 + .ascii "mutable-globals" + .int8 43 + .int8 15 + .ascii "reference-types" + .int8 43 + .int8 8 + .ascii "sign-ext" + .section .debug_str,"S",@ + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/globals.s b/extensions/cxx_debugging/tests/inputs/globals.s new file mode 100644 index 0000000..ac2821c --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/globals.s @@ -0,0 +1,443 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + i32.const 0 +.Lsub_main: + return + end_function +.L__original_main_end: + + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + .int8 1 # Abbrev [1] DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .Lsub_main # DW_AT_high_pc + .int32 .Lcomp_dir # DW_AT_comp_dir + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_1 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_2 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_3 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_4 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_5 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_6 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_7 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_8 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_9 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_10 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_11 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_12 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_13 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_14 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_15 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_16 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_17 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_18 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_19 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_20 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_21 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_22 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_23 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_24 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_25 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_26 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_27 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_28 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_29 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_30 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_31 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_32 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_33 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_34 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_35 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_36 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_37 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_38 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_39 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_40 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_41 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_42 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 + .int8 2 # Abbrev [2] DW_TAG_variable + .int32 .Lvar_name_43 # DW_AT_name + .int32 .Ltype # DW_AT_type + .int8 5 # DW_AT_location + .int8 0x3 # DW_OP_addr + .int32 0x00 +.Ltype: + .int8 3 # Abbrev [3] DW_TAG_type + .int32 .Ltype_name # DW_AT_name + .int8 0x07 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end0: + .int32 .Ldebug_info_end1-.Ldebug_info_start1 # Length of Unit +.Ldebug_info_start1: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + ${ABBR} 1 # DW_TAG_compile_unit + .int32 .Lsub_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lcomp_dir # DW_AT_comp_dir + ${ABBR} 2 # DW_TAG_variable + .int32 .Lvar_name_separate_cu # DW_AT_name + .int32 .Ltype2 # DW_AT_type + .int8 5 # DW_AT_location + ${DW_OP_addr} + .int32 0x00 +.Ltype2: + .int8 3 # Abbrev [3] DW_TAG_type + .int32 .Ltype_name # DW_AT_name + .int8 0x07 # DW_AT_encoding + .int8 4 # DW_AT_byte_size + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end1: + + .section .debug_str,"S",@ +.Lcomp_dir: + .asciz "" +.Ltype_name: + .asciz "int" +.Lvar_name_1: + .asciz "i" +.Lvar_name_2: + .asciz "S::buffer" +.Lvar_name_3: + .asciz "::i_ptr" +.Lvar_name_4: + .asciz "::v_ptr" +.Lvar_name_5: + .asciz "::var_bool" +.Lvar_name_6: + .asciz "::var_char" +.Lvar_name_7: + .asciz "::var_double" +.Lvar_name_8: + .asciz "::var_float" +.Lvar_name_9: + .asciz "::var_int" +.Lvar_name_10: + .asciz "::var_int16_t" +.Lvar_name_11: + .asciz "::var_int32_t" +.Lvar_name_12: + .asciz "::var_int64_t" +.Lvar_name_13: + .asciz "::var_int8_t" +.Lvar_name_14: + .asciz "::var_long" +.Lvar_name_15: + .asciz "::var_long_int" +.Lvar_name_16: + .asciz "::var_long_long" +.Lvar_name_17: + .asciz "::var_long_long_int" +.Lvar_name_18: + .asciz "::var_short" +.Lvar_name_19: + .asciz "::var_short_int" +.Lvar_name_20: + .asciz "::var_signed" +.Lvar_name_21: + .asciz "::var_signed_char" +.Lvar_name_22: + .asciz "::var_signed_int" +.Lvar_name_23: + .asciz "::var_signed_long" +.Lvar_name_24: + .asciz "::var_signed_long_int" +.Lvar_name_25: + .asciz "::var_signed_long_long" +.Lvar_name_26: + .asciz "::var_signed_long_long_int" +.Lvar_name_27: + .asciz "::var_signed_short" +.Lvar_name_28: + .asciz "::var_signed_short_int" +.Lvar_name_29: + .asciz "::var_uint16_t" +.Lvar_name_30: + .asciz "::var_uint32_t" +.Lvar_name_31: + .asciz "::var_uint64_t" +.Lvar_name_32: + .asciz "::var_uint8_t" +.Lvar_name_33: + .asciz "::var_unsigned" +.Lvar_name_34: + .asciz "::var_unsigned_char" +.Lvar_name_35: + .asciz "::var_unsigned_int" +.Lvar_name_36: + .asciz "::var_unsigned_long" +.Lvar_name_37: + .asciz "::var_unsigned_long_int" +.Lvar_name_38: + .asciz "::var_unsigned_long_long" +.Lvar_name_39: + .asciz "::var_unsigned_long_long_int" +.Lvar_name_40: + .asciz "::var_unsigned_short" +.Lvar_name_41: + .asciz "::var_unsigned_short_int" +.Lvar_name_42: + .asciz "::var_int128" +.Lvar_name_43: + .asciz "::var_uint128" +.Lvar_name_separate_cu: + .asciz "::var_separate_cu" + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + ${DW_FORM_addr} + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 2 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x49 # DW_AT_type + .int8 0x10 # DW_FORM_ref_addr + .int8 2 # DW_AT_location + .int8 24 # DW_FORM_exprloc + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 3 # Abbreviation Code + .int8 0x24 # DW_TAG_base_type + .int8 0 # DW_CHILDREN_no + .int8 3 # DW_AT_name + .int8 14 # DW_FORM_strp + .int8 0x3e # DW_AT_encoding + .int8 0x0b # DW_FORM_data1 + .int8 0x0b # DW_AT_byte_size + .int8 0x0b # DW_FORM_data1 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) diff --git a/extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s b/extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s new file mode 100644 index 0000000..709fc61 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s @@ -0,0 +1,92 @@ +# Copyright 2024 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .file "hello-split-missing-dwo.c" + .functype hello_missing_dwo () -> () + .section .text.hello_missing_dwo,"",@ + .hidden hello_missing_dwo + .globl hello_missing_dwo + .type hello_missing_dwo,@function +hello_missing_dwo: +.Lfunc_begin0: + .file 0 "." "hello-split-missing-dwo.c" md5 0x393a76bb56172cb0b207e2046f024d56 + .loc 0 1 0 # hello-split-missing-dwo.c:1:0 + .functype hello_missing_dwo () -> () + .loc 0 1 26 prologue_end # hello-split-missing-dwo.c:1:26 + return + end_function +.Ltmp0: +.Lfunc_end0: + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_skeleton_unit} + ${DW_CHILDREN_no} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${DW_AT_str_offsets_base} + ${DW_FORM_sec_offset} + ${DW_AT_comp_dir} + ${DW_FORM_strx1} + .ascii "\264B" + ${DW_FORM_flag_present} + ${DW_AT_dwo_name} + ${DW_FORM_strx1} + ${DW_AT_low_pc} + ${DW_FORM_addrx} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${DW_AT_addr_base} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + ${EOM} + .section .debug_info,"",@ +.Lcu_begin0: + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 5 # DWARF version number + .int8 4 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 .debug_abbrev0 # Offset Into Abbrev. Section + .int64 -3645740354542567922 + ${ABBR} 1 # Abbrev [1] 0x14:0x14 DW_TAG_skeleton_unit + .int32 .Lline_table_start0 # DW_AT_stmt_list + .int32 .Lstr_offsets_base0 # DW_AT_str_offsets_base + .int8 0 # DW_AT_comp_dir + # DW_AT_GNU_pubnames + .int8 1 # DW_AT_dwo_name + .int8 0 # DW_AT_low_pc + .int32 .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc + .int32 .Laddr_table_base0 # DW_AT_addr_base +.Ldebug_info_end0: + .section .debug_str_offsets,"",@ + .int32 12 + .int16 5 + .int16 0 +.Lstr_offsets_base0: + .section .debug_str,"S",@ +.Lskel_string0: + .asciz "." +.Lskel_string1: + .asciz "hello-split-missing-dwo.dwo" + .section .debug_str_offsets,"",@ + .int32 .Lskel_string0 + .int32 .Lskel_string1 + +# Removed dwo sections, as we will remove the dwo file anyway. +# This will generate an empty .dwo file. + + .section .debug_addr,"",@ + .int32 .Ldebug_addr_end0-.Ldebug_addr_start0 # Length of contribution +.Ldebug_addr_start0: + .int16 5 # DWARF version number + .int8 4 # Address size + .int8 0 # Segment selector size +.Laddr_table_base0: + .int32 .Lfunc_begin0 +.Ldebug_addr_end0: + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/hello-split.s b/extensions/cxx_debugging/tests/inputs/hello-split.s new file mode 100644 index 0000000..b93f4da --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/hello-split.s @@ -0,0 +1,134 @@ +# Copyright 2024 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .file "hello-split.c" + .functype hello2 () -> () + .section .text.hello2,"",@ + .hidden hello2 + .globl hello2 + .type hello2,@function +hello2: +.Lfunc_begin0: + .file 0 "." "hello-split.c" md5 0xd7627bae6ae840ffc57a322e277e92fd + .loc 0 1 0 # hello-split.c:1:0 + .functype hello2 () -> () +# %bb.0: + .loc 0 2 2 prologue_end # hello-split.c:2:2 + return + end_function +.Ltmp0: +.Lfunc_end0: + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_skeleton_unit} + ${DW_CHILDREN_no} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${DW_AT_str_offsets_base} + ${DW_FORM_sec_offset} + ${DW_AT_comp_dir} + ${DW_FORM_strx1} + .ascii "\264B" + ${DW_FORM_flag_present} + ${DW_AT_dwo_name} + ${DW_FORM_strx1} + ${DW_AT_low_pc} + ${DW_FORM_addrx} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${DW_AT_addr_base} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + ${EOM} + .section .debug_info,"",@ +.Lcu_begin0: + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 5 # DWARF version number + .int8 4 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 .debug_abbrev0 # Offset Into Abbrev. Section + .int64 -4350062774228680187 + ${ABBR} 1 + .int32 .Lline_table_start0 # DW_AT_stmt_list + .int32 .Lstr_offsets_base0 # DW_AT_str_offsets_base + .int8 0 # DW_AT_comp_dir + # DW_AT_GNU_pubnames + .int8 1 # DW_AT_dwo_name + .int8 0 # DW_AT_low_pc + .int32 .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc + .int32 .Laddr_table_base0 # DW_AT_addr_base +.Ldebug_info_end0: + .section .debug_str_offsets,"",@ + .int32 12 + .int16 5 + .int16 0 +.Lstr_offsets_base0: + .section .debug_str,"S",@ +.Lskel_string0: + .asciz "." +.Lskel_string1: + .asciz "hello-split.dwo" + .section .debug_str_offsets,"",@ + .int32 .Lskel_string0 + .int32 .Lskel_string1 + .section .debug_str_offsets.dwo,"",@ + .int32 20 + .int16 5 + .int16 0 + .section .debug_str.dwo,"S",@ +.Ldwo_string0: + .asciz "Handwritten and clang" +.Ldwo_string1: + .asciz "hello-split.c" +.Ldwo_string2: + .asciz "hello-split.dwo" + .section .debug_str_offsets.dwo,"",@ + .int32 0 + .int32 .Ldwo_string1-.Ldwo_string0 + .int32 .Ldwo_string2-.Ldwo_string1 + .section .debug_info.dwo,"",@ + .int32 .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit +.Ldebug_info_dwo_start0: + .int16 5 # DWARF version number + .int8 5 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 0 # Offset Into Abbrev. Section + .int64 -4350062774228680187 + ${ABBR} 1 # Abbrev [1] 0x14:0x18 DW_TAG_compile_unit + .int8 0 # DW_AT_producer + .int16 29 # DW_AT_language + .int8 1 # DW_AT_name + .int8 2 # DW_AT_dwo_name + ${EOM} # End Of Children Mark +.Ldebug_info_dwo_end0: + .section .debug_abbrev.dwo,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_no} + ${DW_AT_producer} + ${DW_FORM_strx1} + ${DW_AT_language} + ${DW_FORM_data2} + ${DW_AT_name} + ${DW_FORM_strx1} + ${DW_AT_dwo_name} + ${DW_FORM_strx1} + ${EOM} + ${EOM} + ${EOM} + .section .debug_addr,"",@ + .int32 .Ldebug_addr_end0-.Ldebug_addr_start0 # Length of contribution +.Ldebug_addr_start0: + .int16 5 # DWARF version number + .int8 4 # Address size + .int8 0 # Segment selector size +.Laddr_table_base0: + .int32 .Lfunc_begin0 +.Ldebug_addr_end0: + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/hello.s b/extensions/cxx_debugging/tests/inputs/hello.s new file mode 100644 index 0000000..120efca --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/hello.s @@ -0,0 +1,70 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + .file 1 "hello.c" + .loc 1 3 0 + i32.const 0 + .file 2 "printf.h" + .loc 2 1 0 + return + end_function +.L__original_main_end: + + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + + ${ABBR} 1 # DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lstring0 # DW_AT_comp_dir + .int32 .Lline_table_start0 # DW_AT_stmt_list + + ${ABBR} 2 # DW_TAG_subprogram + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + ${EOM} # End DW_TAG_subprogram +.Ldebug_info_end0: + + .section .debug_str,"S",@ +.Lstring0: + .asciz "" + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + + ${ABBR} 2 + ${DW_TAG_subprogram} + ${DW_CHILDREN_no} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + ${EOM} + + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/helper.s b/extensions/cxx_debugging/tests/inputs/helper.s new file mode 100644 index 0000000..c91eba3 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/helper.s @@ -0,0 +1,130 @@ +# Copyright 2024 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .file "helper.c" + .section .text.help,"",@ + .hidden help + .globl help + .type help,@function +help: +.Lfunc_begin0: + .file 0 "." "helper.c" md5 0xc7cea26288f845fdc61a299be6b58fe8 + .loc 0 1 0 # helper.c:1:0 + .functype help () -> () + .loc 0 2 3 prologue_end # helper.c:2:3 + return + end_function +.Lfunc_end0: + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_skeleton_unit} + ${DW_CHILDREN_no} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${DW_AT_str_offsets_base} + ${DW_FORM_sec_offset} + ${DW_AT_comp_dir} + ${DW_FORM_strx1} + .ascii "\264B" # DW_AT_GNU_pubnames + ${DW_FORM_flag_present} + ${DW_AT_dwo_name} + ${DW_FORM_strx1} + ${DW_AT_low_pc} + ${DW_FORM_addrx} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${DW_AT_addr_base} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + ${EOM} + .section .debug_info,"",@ +.Lcu_begin0: + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 5 # DWARF version number + .int8 4 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 .debug_abbrev0 # Offset Into Abbrev. Section + .int64 -4626675830171985256 + ${ABBR} 1 # Abbrev [1] 0x14:0x14 DW_TAG_skeleton_unit + .int32 .Lline_table_start0 # DW_AT_stmt_list + .int32 .Lstr_offsets_base0 # DW_AT_str_offsets_base + .int8 0 # DW_AT_comp_dir + # DW_AT_GNU_pubnames + .int8 1 # DW_AT_dwo_name + .int8 0 # DW_AT_low_pc + .int32 .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc + .int32 .Laddr_table_base0 # DW_AT_addr_base +.Ldebug_info_end0: + .section .debug_str_offsets,"",@ + .int32 12 + .int16 5 + .int16 0 +.Lstr_offsets_base0: + .section .debug_str,"S",@ +.Lskel_string0: + .asciz "." +.Lskel_string1: + .asciz "helper.dwo" + .section .debug_str_offsets,"",@ + .int32 .Lskel_string0 + .int32 .Lskel_string1 + .section .debug_str_offsets.dwo,"",@ + .int32 20 + .int16 5 + .int16 0 + .section .debug_str.dwo,"S",@ +.Ldwo_string0: + .asciz "Handwritten and clang" +.Ldwo_string1: + .asciz "helper.c" +.Ldwo_string2: + .asciz "helper.dwo" + .section .debug_str_offsets.dwo,"",@ + .int32 0 + .int32 .Ldwo_string1-.Ldwo_string0 + .int32 .Ldwo_string2-.Ldwo_string1 + .section .debug_info.dwo,"",@ + .int32 .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit +.Ldebug_info_dwo_start0: + .int16 5 # DWARF version number + .int8 5 # DWARF Unit Type + .int8 4 # Address Size (in bytes) + .int32 0 # Offset Into Abbrev. Section + .int64 -4626675830171985256 + ${ABBR} 1 # Abbrev [1] 0x14:0x18 DW_TAG_compile_unit + .int8 0 # DW_AT_producer + .int16 29 # DW_AT_language + .int8 1 # DW_AT_name + .int8 2 # DW_AT_dwo_name + ${EOM} # End Of Children Mark +.Ldebug_info_dwo_end0: + .section .debug_abbrev.dwo,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_no} + ${DW_AT_producer} + ${DW_FORM_strx1} + ${DW_AT_language} + ${DW_FORM_data2} + ${DW_AT_name} + ${DW_FORM_strx1} + ${DW_AT_dwo_name} + ${DW_FORM_strx1} + ${EOM} + ${EOM} + ${EOM} + .section .debug_addr,"",@ + .int32 .Ldebug_addr_end0-.Ldebug_addr_start0 # Length of contribution +.Ldebug_addr_start0: + .int16 5 # DWARF version number + .int8 4 # Address size + .int8 0 # Segment selector size +.Laddr_table_base0: + .int32 .Lfunc_begin0 +.Ldebug_addr_end0: + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/inline.s b/extensions/cxx_debugging/tests/inputs/inline.s new file mode 100644 index 0000000..216bb3f --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/inline.s @@ -0,0 +1,196 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + .file 1 "inline.c" + .loc 1 1 0 + i32.const 0 +.L__inline_begin: + .loc 1 10 0 + return +.L__inline_end: + .loc 1 2 0 + end_function +.L__original_main_end: + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + + ${ABBR} 1 # DW_TAG_compile_unit + .int32 .Lcomp_dir_str # DW_AT_comp_dir + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lline_table_start0 # DW_AT_stmt_list + +.Linline_fn_def: + ${ABBR} 2 # DW_TAG_subprogram + .int32 .Linlined_fn_name_str # DW_AT_name + ${DW_INL_inlined} # DW_AT_inline + +.Lvar_def: + ${ABBR} 5 # DW_TAG_variable + .int32 .Lvarname_str # DW_AT_name + +.Lparam_def: + ${ABBR} 6 # DW_TAG_formal_parameter + .int32 .Lparamname_str # DW_AT_name + ${EOM} # End DW_TAG_subprogram + + ${ABBR} 3 # DW_TAG_subprogram + .int32 .Lcalling_fn_name_str # DW_AT_name + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + + ${ABBR} 9 # DW_TAG_variable + # DW_AT_location + .int8 2 + ${DW_OP_fbreg} + .int8 0 + .int32 .Louter_varvame_str # DW_AT_name + + ${ABBR} 4 # DW_TAG_inlined_subroutine + .int32 .Linline_fn_def # DW_AT_abstract_origin + .int32 .L__inline_begin # DW_AT_low_pc + .int32 .L__inline_end # DW_AT_high_pc + + ${ABBR} 7 # DW_TAG_variable + # DW_AT_location + .int8 2 + ${DW_OP_fbreg} + .int8 0 + .int32 .Lvar_def # DW_AT_abstract_origin + + ${ABBR} 8 # DW_TAG_formal_parameter + # DW_AT_location + .int8 2 + ${DW_OP_fbreg} + .int8 0 + .int32 .Lparam_def # DW_AT_abstract_origin + ${EOM} #End DW_TAG_inlined subroutine + + ${EOM} # End DW_TAG_subprogram + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end0: + + .section .debug_str,"S",@ +.Lcomp_dir_str: + .asciz "" +.Linlined_fn_name_str: + .asciz "callee" +.Lcalling_fn_name_str: + .asciz "caller" +.Louter_varvame_str: + .asciz "outer_var" +.Lvarname_str: + .asciz "inner_var" +.Lparamname_str: + .asciz "inner_param" + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + + ${ABBR} 2 + ${DW_TAG_subprogram} + ${DW_CHILDREN_yes} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_inline} + ${DW_FORM_data1} + ${EOM} + ${EOM} + + ${ABBR} 3 + ${DW_TAG_subprogram} + ${DW_CHILDREN_yes} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + + ${ABBR} 4 + ${DW_TAG_inlined_subroutine} + ${DW_CHILDREN_yes} + ${DW_AT_abstract_origin} + ${DW_FORM_ref_addr} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + + ${ABBR} 5 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + + ${ABBR} 6 + ${DW_TAG_formal_parameter} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + + ${ABBR} 7 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_abstract_origin} + ${DW_FORM_ref_addr} + ${EOM} + ${EOM} + + ${ABBR} 8 + ${DW_TAG_formal_parameter} + ${DW_CHILDREN_no} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_abstract_origin} + ${DW_FORM_ref_addr} + ${EOM} + ${EOM} + + ${ABBR} 9 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + + ${EOM} + + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/tests/inputs/namespaces.s b/extensions/cxx_debugging/tests/inputs/namespaces.s new file mode 100644 index 0000000..859bfaf --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/namespaces.s @@ -0,0 +1,207 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.L__original_main_end: + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + ${ABBR} 1 # DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lcomp_dir_str # DW_AT_comp_dir +.Lint_type: + ${ABBR} 8 # DW_TAG_base_type + .int32 .Lint_str # DW_AT_name + ${ABBR} 2 # DW_TAG_namespace + .int32 .Ln1_str # DW_AT_name + ${ABBR} 2 # DW_TAG_namespace + .int32 .Ln2_str # DW_AT_name + ${ABBR} 4 # DW_TAG_variable + .int32 .Lvarname_str # DW_AT_name + .int32 .Lint_type # DW_AT_type + .int8 1 # DW_AT_external + .int8 5 # DW_AT_location + ${DW_OP_addr} + .int32 0x00 + .int32 .Llinkagename1_str # DW_AT_linkage_name + ${EOM} # End DW_TAG_namespace + ${ABBR} 4 # DW_TAG_variable + .int32 .Lvarname_str # DW_AT_name + .int32 .Lint_type # DW_AT_type + .int8 0 # DW_AT_external + .int8 5 # DW_AT_location + ${DW_OP_addr} + .int32 0x00 + .int32 .Llinkagename2_str # DW_AT_linkage_name + ${ABBR} 5 # DW_TAG_variable + .int32 .Lstatic_var_addr # DW_AT_specification + .int8 5 # DW_AT_location + ${DW_OP_addr} + .int32 0x00 + .int32 .Llinkagename3_str # DW_AT_linkage_name + ${ABBR} 6 # DW_TAG_class_type + .int32 .Lclassname_str # DW_AT_name +.Lstatic_var_addr: + ${ABBR} 7 # DW_TAG_member + .int32 .Lvarname_str # DW_AT_name + .int32 .Lint_type # DW_AT_type + .int8 1 # DW_AT_external + .int8 1 # DW_AT_declaration + ${EOM} # End DW_TAG_class_type + ${EOM} # End DW_TAG_namespace + ${ABBR} 9 # DW_TAG_namespace + ${ABBR} 4 # DW_TAG_variable + .int32 .Lvarname_str # DW_AT_name + .int32 .Lint_type # DW_AT_type + .int8 1 # DW_AT_external + .int8 5 # DW_AT_location + ${DW_OP_addr} + .int32 0x00 + .int32 .Llinkagename4_str # DW_AT_linkage_name + ${EOM} # End DW_TAG_namespace + ${ABBR} 10 # DW_TAG_subprogram + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + ${ABBR} 4 # DW_TAG_variable + .int32 .Lvarname3_str # DW_AT_name + .int32 .Lint_type # DW_AT_type + .int8 1 # DW_AT_external + # DW_AT_location + .int8 4 + ${DW_OP_WASM_location} + .int8 0x0 + .int8 0x1 + ${DW_OP_stack_value} + .int32 .Llinkagename5_str # DW_AT_linkage_name + ${EOM} # End DW_TAG_subprogram + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end0: + + .section .debug_str,"S",@ +.Lcomp_dir_str: + .asciz "/tmp" +.Ln1_str: + .asciz "n1" +.Ln2_str: + .asciz "n2" +.Lvarname_str: + .asciz "I" +.Lclassname_str: + .asciz "MyClass" +.Lvarname2_str: + .asciz "K" +.Lvarname3_str: + .asciz "L" +.Llinkagename1_str: + .asciz "_ZN2n12n21IE" +.Llinkagename2_str: + .asciz "_ZN2n11IE" +.Llinkagename3_str: + .asciz "_ZN2n17MyClass1IE" +.Llinkagename4_str: + .asciz "_ZN12_GLOBAL__N_11KE" +.Llinkagename5_str: + .asciz "L" +.Lint_str: + .asciz "int" + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_data4} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 2 + ${DW_TAG_namespace} + ${DW_CHILDREN_yes} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 4 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_type} + ${DW_FORM_ref_addr} + ${DW_AT_external} + ${DW_FORM_flag} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_linkage_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 5 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_specification} + ${DW_FORM_ref_addr} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${DW_AT_linkage_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 6 + ${DW_TAG_class_type} + ${DW_CHILDREN_yes} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 7 + ${DW_TAG_member} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_type} + ${DW_FORM_ref_addr} + ${DW_AT_external} + ${DW_FORM_flag} + ${DW_AT_declaration} + ${DW_FORM_flag} + ${EOM} + ${EOM} + ${ABBR} 8 + ${DW_TAG_base_type} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 9 + ${DW_TAG_namespace} + ${DW_CHILDREN_yes} + ${EOM} + ${EOM} + ${ABBR} 10 + ${DW_TAG_subprogram} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + ${EOM} diff --git a/extensions/cxx_debugging/tests/inputs/shadowing.s b/extensions/cxx_debugging/tests/inputs/shadowing.s new file mode 100644 index 0000000..80f6052 --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/shadowing.s @@ -0,0 +1,121 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +.text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.L__original_main_end: + + .section .debug_info,"",@ + .int32 .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + ${ABBR} 1 # DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lcomp_dir_str # DW_AT_comp_dir +.Ltype_int: + ${ABBR} 2 # DW_TAG_base_type + .int32 .Ltype_int_str # DW_AT_name + ${ABBR} 3 # DW_TAG_subprogram + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lfn_name_str # DW_AT_name + ${ABBR} 4 # DW_TAG_formal_parameter + .int32 .Lvar_name_str # DW_AT_name + .int32 .Ltype_int # DW_AT_type + ${ABBR} 5 # DW_TAG_lexical_block + .int32 __original_main+1 # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + ${ABBR} 6 # DW_TAG_variable + .int32 .Lvar_name_str # DW_AT_name + .int32 .Ltype_int # DW_AT_type + # DW_AT_location + .int8 2 + ${DW_OP_fbreg} + .int8 0x0 + ${EOM} # End DW_TAG_lexical_block + ${EOM} # End DW_TAG_subprogram + ${EOM} # End DW_TAG_compile_unit +.Ldebug_info_end: + + .section .debug_str,"S",@ +.Lcomp_dir_str: + .asciz "/tmp" +.Ltype_int_str: + .asciz "int" +.Ltype_float_str: + .asciz "float" +.Lfn_name_str: + .asciz "fn" +.Lvar_name_str: + .asciz "a" + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 2 + ${DW_TAG_base_type} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 3 + ${DW_TAG_subprogram} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${DW_AT_name} + ${DW_FORM_strp} + ${EOM} + ${EOM} + ${ABBR} 4 + ${DW_TAG_formal_parameter} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_type} + ${DW_FORM_ref_addr} + ${EOM} + ${EOM} + ${ABBR} 5 + ${DW_TAG_lexical_block} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + ${ABBR} 6 + ${DW_TAG_variable} + ${DW_CHILDREN_no} + ${DW_AT_name} + ${DW_FORM_strp} + ${DW_AT_type} + ${DW_FORM_ref_addr} + ${DW_AT_location} + ${DW_FORM_exprloc} + ${EOM} + ${EOM} + ${EOM} diff --git a/extensions/cxx_debugging/tests/inputs/split-dwarf.s b/extensions/cxx_debugging/tests/inputs/split-dwarf.s new file mode 100644 index 0000000..7ea2cfc --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/split-dwarf.s @@ -0,0 +1,126 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + # Need a non-empty code section for variable lookups to be able to find the + # comp unit by code offset. + .file "split-dwarf.c" + .globl __original_main + .type __original_main,@function +__original_main: # @__original_main + .functype __original_main () -> (i32) + i32.const 0 + return + end_function +.Lfunc_end0: + .size __original_main, .Lfunc_end0-__original_main + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + .int8 1 # Abbrev [1] DW_TAG_compile_unit + .int32 0x8 # DW_AT_low_pc + .int32 0x10 # DW_AT_high_pc + .int32 .Lstring0 # DW_AT_comp_dir + .int32 .Lstring1 # DW_AT_GNU_dwo_name + .int64 0x12345 # DW_AT_GNU_dwo_id + .int32 .Laddr_table_base0 # DW_AT_GNU_addr_base +.Ldebug_info_end0: + + .section .debug_addr,"",@ +.Laddr_table_base0: + .int32 0xbeef + + .section .debug_str,"S",@ +.Lstring0: + .asciz "." +.Lstring1: + .asciz "split-dwarf.s.dwo" + + .section .debug_abbrev,"",@ + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 0 # DW_CHILDREN_no + .int8 17 # DW_AT_low_pc + .int8 1 # DW_FORM_addr + .int8 18 # DW_AT_high_pc + .int8 6 # DW_FORM_data4 + .int8 27 # DW_AT_comp_dir + .int8 14 # DW_FORM_strp + .ascii "\260B" # DW_AT_GNU_dwo_name + .int8 14 # DW_FORM_strp + .ascii "\261B" # DW_AT_GNU_dwo_id + .int8 7 # DW_FORM_data8 + .ascii "\263B" # DW_AT_GNU_addr_base + .int8 23 # DW_FORM_sec_offset + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) + + .section .debug_info.dwo,"",@ + .int32 .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit +.Ldebug_info_dwo_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + .int8 1 # Abbrev [1] DW_TAG_compile_unit + .int8 3 # DW_AT_producer + .int16 12 # DW_AT_language + .int8 0 # DW_AT_name + .int8 1 # DW_AT_GNU_dwo_name + .int64 0x12345 # DW_AT_GNU_dwo_id + .int8 2 # Abbrev [2] DW_TAG_variable + .int8 2 # DW_AT_location + .int8 0xfb # DW_OP_GNU_addr_index + .int8 0 + .int8 2 # DW_AT_name +.Ldebug_info_dwo_end0: + + .section .debug_str.dwo,"S",@ +.Ldebug_str_dwo0: + .asciz "string-split.c" +.Ldebug_str_dwo1: + .asciz "string-split.s.dwo" +.Ldebug_str_dwo2: + .asciz "global_var" +.Ldebug_str_dwo3: + .asciz "Handwritten DWARF" +.Ldebug_str_dwo_end: + + .section .debug_str_offsets.dwo,"",@ + .int32 .Ldebug_str_dwo0-.Ldebug_str_dwo0 + .int32 .Ldebug_str_dwo1-.Ldebug_str_dwo0 + .int32 .Ldebug_str_dwo2-.Ldebug_str_dwo0 + .int32 .Ldebug_str_dwo3-.Ldebug_str_dwo0 + + .section .debug_abbrev.dwo,"",@ +.Ldebug_abbrev_dwo0: + .int8 1 # Abbreviation Code + .int8 17 # DW_TAG_compile_unit + .int8 1 # DW_CHILDREN_yes + .int8 37 # DW_AT_producer + .ascii "\202>" # DW_FORM_GNU_str_index + .int8 19 # DW_AT_language + .int8 5 # DW_FORM_data2 + .int8 3 # DW_AT_name + .ascii "\202>" # DW_FORM_GNU_str_index + .ascii "\260B" # DW_AT_GNU_dwo_name + .ascii "\202>" # DW_FORM_GNU_str_index + .ascii "\261B" # DW_AT_GNU_dwo_id + .int8 7 # DW_FORM_data8 + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 2 # Abbreviation Code + .int8 52 # DW_TAG_variable + .int8 0 # DW_CHILDREN_no + .int8 2 # DW_AT_location + .int8 24 # DW_FORM_exprloc + .int8 3 # DW_AT_name + .ascii "\202>" # DW_FORM_GNU_str_index + .int8 0 # EOM(1) + .int8 0 # EOM(2) + .int8 0 # EOM(3) diff --git a/extensions/cxx_debugging/tests/inputs/string_view.cc b/extensions/cxx_debugging/tests/inputs/string_view.cc new file mode 100644 index 0000000..5277c1a --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/string_view.cc @@ -0,0 +1,40 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +struct MyStringView { + explicit MyStringView(const std::string& s) + : the_string_(s), begin_(0), end_(s.size()) {} + MyStringView(const std::string& s, size_t begin, size_t length) + : the_string_(s), begin_(begin), end_(begin + length) {} + + // NOLINTNEXTLINE + auto begin() const { return the_string_.begin() + begin_; } + // NOLINTNEXTLINE + auto end() const { return the_string_.begin() + end_; } + // NOLINTNEXTLINE + auto str() const { return the_string_.substr(begin_, end_ - begin_); } + // NOLINTNEXTLINE + auto size() const { return end_ - begin_; } + + private: + // const std::string& the_string_; + const std::string& the_string_; + const size_t begin_, end_; +}; + +std::ostream& operator<<(std::ostream& o, const MyStringView& v) { + return o << v.str(); +} + +int main() { + std::string my_string = "Hello World!"; + MyStringView full_view(my_string); + std::cout << "Full string: " << full_view << " (size=" << full_view.size() + << ")\n"; + MyStringView hello(my_string, 0, 6); + std::cout << "Hello string: " << hello << " (size=" << hello.size() << ")\n"; +} diff --git a/extensions/cxx_debugging/tests/inputs/windows_paths.s b/extensions/cxx_debugging/tests/inputs/windows_paths.s new file mode 100644 index 0000000..29c7c4b --- /dev/null +++ b/extensions/cxx_debugging/tests/inputs/windows_paths.s @@ -0,0 +1,70 @@ +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + .text + .globl __original_main + .type __original_main,@function +__original_main: + .functype __original_main () -> (i32) + .file 1 "temp.c" + .loc 1 5 5 + i32.const 0 + return + end_function +.L__original_main_end: + + + .section .debug_info,"",@ + .int32 .Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit +.Ldebug_info_start0: + .int16 4 # DWARF version number + .int32 0 # Offset Into Abbrev. Section + .int8 4 # Address Size (in bytes) + ${ABBR} 1 # DW_TAG_compile_unit + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + .int32 .Lstring0 # DW_AT_comp_dir + .int32 .Lline_table_start0 # DW_AT_stmt_list + + ${ABBR} 2 # DW_TAG_subprogram + .int32 __original_main # DW_AT_low_pc + .int32 .L__original_main_end # DW_AT_high_pc + ${EOM} +.Ldebug_info_end0: + + .section .debug_addr,"",@ +.Laddr_table_base0: + .int32 0xbeef + + .section .debug_str,"S",@ +.Lstring0: + .asciz "C:\\src" + + .section .debug_abbrev,"",@ + ${ABBR} 1 + ${DW_TAG_compile_unit} + ${DW_CHILDREN_yes} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${DW_AT_comp_dir} + ${DW_FORM_strp} + ${DW_AT_stmt_list} + ${DW_FORM_sec_offset} + ${EOM} + ${EOM} + ${ABBR} 2 + ${DW_TAG_subprogram} + ${DW_CHILDREN_no} + ${DW_AT_low_pc} + ${DW_FORM_addr} + ${DW_AT_high_pc} + ${DW_FORM_addr} + ${EOM} + ${EOM} + ${EOM} + + .section .debug_line,"",@ +.Lline_table_start0: diff --git a/extensions/cxx_debugging/third_party/.clang-format b/extensions/cxx_debugging/third_party/.clang-format new file mode 100644 index 0000000..e384528 --- /dev/null +++ b/extensions/cxx_debugging/third_party/.clang-format @@ -0,0 +1 @@ +DisableFormat: true diff --git a/extensions/cxx_debugging/third_party/.gitignore b/extensions/cxx_debugging/third_party/.gitignore new file mode 100644 index 0000000..7c52414 --- /dev/null +++ b/extensions/cxx_debugging/third_party/.gitignore @@ -0,0 +1,2 @@ +llvm/src +lldb-eval/src From 0ee38d9461fd7bc06efdea36fdf4d54f4b5d6721 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Thu, 10 Apr 2025 08:40:21 +1000 Subject: [PATCH 2/9] Move 'extensions/cxx_debugging' files to 'src', 'test' and 'wasm/symbols-backend' folders --- {extensions/cxx_debugging/src => src}/CustomFormatters.ts | 0 .../src/DWARFSymbols.ts => src/DWARFLanguageExtensionPlugin.ts | 0 {extension-api => src}/ExtensionAPI.d.ts | 0 {extensions/cxx_debugging/src => src}/Formatters.ts | 0 {extensions/cxx_debugging/src => src}/MEMFSResourceLoader.ts | 0 {extensions/cxx_debugging/src => src}/ModuleConfiguration.ts | 0 .../src/DevToolsPluginWorker.ts => src/RPCInterface.ts | 0 {extensions/cxx_debugging/src => src}/SymbolsBackend.d.ts | 0 {extensions/cxx_debugging/src => src}/WasmTypes.ts | 0 {extensions/cxx_debugging/src => src}/WorkerRPC.ts | 0 {extensions/cxx_debugging/tests => test}/CustomFormatters_test.ts | 0 .../tests/Formatters_test.ts => test/Formatters.test.ts | 0 {extensions/cxx_debugging/tests => test}/Interpreter_test.ts | 0 .../cxx_debugging/tests => test}/ModuleConfiguration_test.ts | 0 {extensions/cxx_debugging/tests => test}/SymbolsBackend_test.ts | 0 {extensions/cxx_debugging/tests => test}/TestUtils.ts | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/cpp_eval.yaml | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/pointer.yaml | 0 .../cxx_debugging => wasm}/e2e/resources/huge-source-file.cc | 0 {extensions/cxx_debugging => wasm}/e2e/resources/pointers.cc | 0 .../e2e/resources/scope-view-non-primitives.c | 0 .../e2e/resources/scope-view-non-primitives.cpp | 0 .../cxx_debugging => wasm}/e2e/resources/scope-view-primitives.c | 0 .../cxx_debugging => wasm}/e2e/resources/stepping-with-state.c | 0 {extensions/cxx_debugging => wasm}/e2e/resources/test_wasm_simd.c | 0 {extensions/cxx_debugging => wasm}/e2e/resources/vector.cc | 0 {extensions/cxx_debugging => wasm}/e2e/resources/wchar.cc | 0 .../e2e/tests => wasm/e2e}/scope_view_non_primitives.yaml | 0 .../e2e/tests => wasm/e2e}/scope_view_non_primitives_cpp.yaml | 0 .../e2e/tests => wasm/e2e}/scope_view_primitives.yaml | 0 .../cxx_debugging/e2e/tests => wasm/e2e}/stepping_with_state.yaml | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/string_view.yaml | 0 .../cxx_debugging/e2e/tests => wasm/e2e}/test_big_dwo.yaml | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/test_loop.yaml | 0 .../cxx_debugging/e2e/tests => wasm/e2e}/test_wasm_simd.yaml | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/vector.yaml | 0 {extensions/cxx_debugging/e2e/tests => wasm/e2e}/wchar.yaml | 0 {extensions/cxx_debugging => wasm/symbols-backend}/CMakeLists.txt | 0 .../cxx_debugging => wasm/symbols-backend}/lib/ApiContext.cc | 0 .../cxx_debugging => wasm/symbols-backend}/lib/ApiContext.h | 0 .../cxx_debugging => wasm/symbols-backend}/lib/CMakeLists.txt | 0 .../cxx_debugging => wasm/symbols-backend}/lib/Expressions.cc | 0 .../cxx_debugging => wasm/symbols-backend}/lib/Expressions.h | 0 .../cxx_debugging => wasm/symbols-backend}/lib/Variables.cc | 0 .../cxx_debugging => wasm/symbols-backend}/lib/Variables.h | 0 .../cxx_debugging => wasm/symbols-backend}/lib/WasmModule.cc | 0 .../cxx_debugging => wasm/symbols-backend}/lib/WasmModule.h | 0 .../symbols-backend}/lib/WasmVendorPlugins.cc | 0 .../symbols-backend}/lib/WasmVendorPlugins.h | 0 {extensions/cxx_debugging => wasm/symbols-backend}/lib/api.h | 0 .../cxx_debugging => wasm/symbols-backend}/src/CMakeLists.txt | 0 .../cxx_debugging => wasm/symbols-backend}/src/SymbolsBackend.cc | 0 .../cxx_debugging => wasm/symbols-backend}/tests/CMakeLists.txt | 0 .../symbols-backend}/tests/LLDBEvalExtensions.h | 0 .../symbols-backend}/tests/LLDBEvalTests.d.ts | 0 .../symbols-backend}/tests/SymbolsBackendTests.d.ts | 0 .../symbols-backend}/tests/WasmModule_test.cc | 0 .../symbols-backend}/tests/inputs/CMakeLists.txt | 0 .../symbols-backend}/tests/inputs/addr_index.s | 0 .../symbols-backend}/tests/inputs/addresses.cc | 0 .../symbols-backend}/tests/inputs/classstatic.s | 0 .../symbols-backend}/tests/inputs/dw_opcodes.def | 0 .../symbols-backend}/tests/inputs/embedded.s | 0 .../cxx_debugging => wasm/symbols-backend}/tests/inputs/enums.s | 0 .../symbols-backend}/tests/inputs/externref.js | 0 .../symbols-backend}/tests/inputs/externref.s | 0 .../cxx_debugging => wasm/symbols-backend}/tests/inputs/globals.s | 0 .../symbols-backend}/tests/inputs/hello-split-missing-dwo.s | 0 .../symbols-backend}/tests/inputs/hello-split.s | 0 .../cxx_debugging => wasm/symbols-backend}/tests/inputs/hello.s | 0 .../cxx_debugging => wasm/symbols-backend}/tests/inputs/helper.s | 0 .../cxx_debugging => wasm/symbols-backend}/tests/inputs/inline.s | 0 .../symbols-backend}/tests/inputs/namespaces.s | 0 .../symbols-backend}/tests/inputs/shadowing.s | 0 .../symbols-backend}/tests/inputs/split-dwarf.s | 0 .../symbols-backend}/tests/inputs/string_view.cc | 0 .../symbols-backend}/tests/inputs/windows_paths.s | 0 .../symbols-backend}/third_party/.clang-format | 0 .../cxx_debugging => wasm/symbols-backend}/third_party/.gitignore | 0 79 files changed, 0 insertions(+), 0 deletions(-) rename {extensions/cxx_debugging/src => src}/CustomFormatters.ts (100%) rename extensions/cxx_debugging/src/DWARFSymbols.ts => src/DWARFLanguageExtensionPlugin.ts (100%) rename {extension-api => src}/ExtensionAPI.d.ts (100%) rename {extensions/cxx_debugging/src => src}/Formatters.ts (100%) rename {extensions/cxx_debugging/src => src}/MEMFSResourceLoader.ts (100%) rename {extensions/cxx_debugging/src => src}/ModuleConfiguration.ts (100%) rename extensions/cxx_debugging/src/DevToolsPluginWorker.ts => src/RPCInterface.ts (100%) rename {extensions/cxx_debugging/src => src}/SymbolsBackend.d.ts (100%) rename {extensions/cxx_debugging/src => src}/WasmTypes.ts (100%) rename {extensions/cxx_debugging/src => src}/WorkerRPC.ts (100%) rename {extensions/cxx_debugging/tests => test}/CustomFormatters_test.ts (100%) rename extensions/cxx_debugging/tests/Formatters_test.ts => test/Formatters.test.ts (100%) rename {extensions/cxx_debugging/tests => test}/Interpreter_test.ts (100%) rename {extensions/cxx_debugging/tests => test}/ModuleConfiguration_test.ts (100%) rename {extensions/cxx_debugging/tests => test}/SymbolsBackend_test.ts (100%) rename {extensions/cxx_debugging/tests => test}/TestUtils.ts (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/cpp_eval.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/pointer.yaml (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/huge-source-file.cc (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/pointers.cc (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/scope-view-non-primitives.c (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/scope-view-non-primitives.cpp (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/scope-view-primitives.c (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/stepping-with-state.c (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/test_wasm_simd.c (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/vector.cc (100%) rename {extensions/cxx_debugging => wasm}/e2e/resources/wchar.cc (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/scope_view_non_primitives.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/scope_view_non_primitives_cpp.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/scope_view_primitives.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/stepping_with_state.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/string_view.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/test_big_dwo.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/test_loop.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/test_wasm_simd.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/vector.yaml (100%) rename {extensions/cxx_debugging/e2e/tests => wasm/e2e}/wchar.yaml (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/CMakeLists.txt (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/ApiContext.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/ApiContext.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/CMakeLists.txt (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/Expressions.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/Expressions.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/Variables.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/Variables.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/WasmModule.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/WasmModule.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/WasmVendorPlugins.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/WasmVendorPlugins.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/lib/api.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/src/CMakeLists.txt (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/src/SymbolsBackend.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/CMakeLists.txt (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/LLDBEvalExtensions.h (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/LLDBEvalTests.d.ts (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/SymbolsBackendTests.d.ts (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/WasmModule_test.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/CMakeLists.txt (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/addr_index.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/addresses.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/classstatic.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/dw_opcodes.def (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/embedded.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/enums.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/externref.js (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/externref.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/globals.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/hello-split-missing-dwo.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/hello-split.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/hello.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/helper.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/inline.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/namespaces.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/shadowing.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/split-dwarf.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/string_view.cc (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/tests/inputs/windows_paths.s (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/third_party/.clang-format (100%) rename {extensions/cxx_debugging => wasm/symbols-backend}/third_party/.gitignore (100%) diff --git a/extensions/cxx_debugging/src/CustomFormatters.ts b/src/CustomFormatters.ts similarity index 100% rename from extensions/cxx_debugging/src/CustomFormatters.ts rename to src/CustomFormatters.ts diff --git a/extensions/cxx_debugging/src/DWARFSymbols.ts b/src/DWARFLanguageExtensionPlugin.ts similarity index 100% rename from extensions/cxx_debugging/src/DWARFSymbols.ts rename to src/DWARFLanguageExtensionPlugin.ts diff --git a/extension-api/ExtensionAPI.d.ts b/src/ExtensionAPI.d.ts similarity index 100% rename from extension-api/ExtensionAPI.d.ts rename to src/ExtensionAPI.d.ts diff --git a/extensions/cxx_debugging/src/Formatters.ts b/src/Formatters.ts similarity index 100% rename from extensions/cxx_debugging/src/Formatters.ts rename to src/Formatters.ts diff --git a/extensions/cxx_debugging/src/MEMFSResourceLoader.ts b/src/MEMFSResourceLoader.ts similarity index 100% rename from extensions/cxx_debugging/src/MEMFSResourceLoader.ts rename to src/MEMFSResourceLoader.ts diff --git a/extensions/cxx_debugging/src/ModuleConfiguration.ts b/src/ModuleConfiguration.ts similarity index 100% rename from extensions/cxx_debugging/src/ModuleConfiguration.ts rename to src/ModuleConfiguration.ts diff --git a/extensions/cxx_debugging/src/DevToolsPluginWorker.ts b/src/RPCInterface.ts similarity index 100% rename from extensions/cxx_debugging/src/DevToolsPluginWorker.ts rename to src/RPCInterface.ts diff --git a/extensions/cxx_debugging/src/SymbolsBackend.d.ts b/src/SymbolsBackend.d.ts similarity index 100% rename from extensions/cxx_debugging/src/SymbolsBackend.d.ts rename to src/SymbolsBackend.d.ts diff --git a/extensions/cxx_debugging/src/WasmTypes.ts b/src/WasmTypes.ts similarity index 100% rename from extensions/cxx_debugging/src/WasmTypes.ts rename to src/WasmTypes.ts diff --git a/extensions/cxx_debugging/src/WorkerRPC.ts b/src/WorkerRPC.ts similarity index 100% rename from extensions/cxx_debugging/src/WorkerRPC.ts rename to src/WorkerRPC.ts diff --git a/extensions/cxx_debugging/tests/CustomFormatters_test.ts b/test/CustomFormatters_test.ts similarity index 100% rename from extensions/cxx_debugging/tests/CustomFormatters_test.ts rename to test/CustomFormatters_test.ts diff --git a/extensions/cxx_debugging/tests/Formatters_test.ts b/test/Formatters.test.ts similarity index 100% rename from extensions/cxx_debugging/tests/Formatters_test.ts rename to test/Formatters.test.ts diff --git a/extensions/cxx_debugging/tests/Interpreter_test.ts b/test/Interpreter_test.ts similarity index 100% rename from extensions/cxx_debugging/tests/Interpreter_test.ts rename to test/Interpreter_test.ts diff --git a/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts b/test/ModuleConfiguration_test.ts similarity index 100% rename from extensions/cxx_debugging/tests/ModuleConfiguration_test.ts rename to test/ModuleConfiguration_test.ts diff --git a/extensions/cxx_debugging/tests/SymbolsBackend_test.ts b/test/SymbolsBackend_test.ts similarity index 100% rename from extensions/cxx_debugging/tests/SymbolsBackend_test.ts rename to test/SymbolsBackend_test.ts diff --git a/extensions/cxx_debugging/tests/TestUtils.ts b/test/TestUtils.ts similarity index 100% rename from extensions/cxx_debugging/tests/TestUtils.ts rename to test/TestUtils.ts diff --git a/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml b/wasm/e2e/cpp_eval.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/cpp_eval.yaml rename to wasm/e2e/cpp_eval.yaml diff --git a/extensions/cxx_debugging/e2e/tests/pointer.yaml b/wasm/e2e/pointer.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/pointer.yaml rename to wasm/e2e/pointer.yaml diff --git a/extensions/cxx_debugging/e2e/resources/huge-source-file.cc b/wasm/e2e/resources/huge-source-file.cc similarity index 100% rename from extensions/cxx_debugging/e2e/resources/huge-source-file.cc rename to wasm/e2e/resources/huge-source-file.cc diff --git a/extensions/cxx_debugging/e2e/resources/pointers.cc b/wasm/e2e/resources/pointers.cc similarity index 100% rename from extensions/cxx_debugging/e2e/resources/pointers.cc rename to wasm/e2e/resources/pointers.cc diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c b/wasm/e2e/resources/scope-view-non-primitives.c similarity index 100% rename from extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c rename to wasm/e2e/resources/scope-view-non-primitives.c diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp b/wasm/e2e/resources/scope-view-non-primitives.cpp similarity index 100% rename from extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp rename to wasm/e2e/resources/scope-view-non-primitives.cpp diff --git a/extensions/cxx_debugging/e2e/resources/scope-view-primitives.c b/wasm/e2e/resources/scope-view-primitives.c similarity index 100% rename from extensions/cxx_debugging/e2e/resources/scope-view-primitives.c rename to wasm/e2e/resources/scope-view-primitives.c diff --git a/extensions/cxx_debugging/e2e/resources/stepping-with-state.c b/wasm/e2e/resources/stepping-with-state.c similarity index 100% rename from extensions/cxx_debugging/e2e/resources/stepping-with-state.c rename to wasm/e2e/resources/stepping-with-state.c diff --git a/extensions/cxx_debugging/e2e/resources/test_wasm_simd.c b/wasm/e2e/resources/test_wasm_simd.c similarity index 100% rename from extensions/cxx_debugging/e2e/resources/test_wasm_simd.c rename to wasm/e2e/resources/test_wasm_simd.c diff --git a/extensions/cxx_debugging/e2e/resources/vector.cc b/wasm/e2e/resources/vector.cc similarity index 100% rename from extensions/cxx_debugging/e2e/resources/vector.cc rename to wasm/e2e/resources/vector.cc diff --git a/extensions/cxx_debugging/e2e/resources/wchar.cc b/wasm/e2e/resources/wchar.cc similarity index 100% rename from extensions/cxx_debugging/e2e/resources/wchar.cc rename to wasm/e2e/resources/wchar.cc diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml b/wasm/e2e/scope_view_non_primitives.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml rename to wasm/e2e/scope_view_non_primitives.yaml diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml b/wasm/e2e/scope_view_non_primitives_cpp.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml rename to wasm/e2e/scope_view_non_primitives_cpp.yaml diff --git a/extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml b/wasm/e2e/scope_view_primitives.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml rename to wasm/e2e/scope_view_primitives.yaml diff --git a/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml b/wasm/e2e/stepping_with_state.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml rename to wasm/e2e/stepping_with_state.yaml diff --git a/extensions/cxx_debugging/e2e/tests/string_view.yaml b/wasm/e2e/string_view.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/string_view.yaml rename to wasm/e2e/string_view.yaml diff --git a/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml b/wasm/e2e/test_big_dwo.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml rename to wasm/e2e/test_big_dwo.yaml diff --git a/extensions/cxx_debugging/e2e/tests/test_loop.yaml b/wasm/e2e/test_loop.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/test_loop.yaml rename to wasm/e2e/test_loop.yaml diff --git a/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml b/wasm/e2e/test_wasm_simd.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml rename to wasm/e2e/test_wasm_simd.yaml diff --git a/extensions/cxx_debugging/e2e/tests/vector.yaml b/wasm/e2e/vector.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/vector.yaml rename to wasm/e2e/vector.yaml diff --git a/extensions/cxx_debugging/e2e/tests/wchar.yaml b/wasm/e2e/wchar.yaml similarity index 100% rename from extensions/cxx_debugging/e2e/tests/wchar.yaml rename to wasm/e2e/wchar.yaml diff --git a/extensions/cxx_debugging/CMakeLists.txt b/wasm/symbols-backend/CMakeLists.txt similarity index 100% rename from extensions/cxx_debugging/CMakeLists.txt rename to wasm/symbols-backend/CMakeLists.txt diff --git a/extensions/cxx_debugging/lib/ApiContext.cc b/wasm/symbols-backend/lib/ApiContext.cc similarity index 100% rename from extensions/cxx_debugging/lib/ApiContext.cc rename to wasm/symbols-backend/lib/ApiContext.cc diff --git a/extensions/cxx_debugging/lib/ApiContext.h b/wasm/symbols-backend/lib/ApiContext.h similarity index 100% rename from extensions/cxx_debugging/lib/ApiContext.h rename to wasm/symbols-backend/lib/ApiContext.h diff --git a/extensions/cxx_debugging/lib/CMakeLists.txt b/wasm/symbols-backend/lib/CMakeLists.txt similarity index 100% rename from extensions/cxx_debugging/lib/CMakeLists.txt rename to wasm/symbols-backend/lib/CMakeLists.txt diff --git a/extensions/cxx_debugging/lib/Expressions.cc b/wasm/symbols-backend/lib/Expressions.cc similarity index 100% rename from extensions/cxx_debugging/lib/Expressions.cc rename to wasm/symbols-backend/lib/Expressions.cc diff --git a/extensions/cxx_debugging/lib/Expressions.h b/wasm/symbols-backend/lib/Expressions.h similarity index 100% rename from extensions/cxx_debugging/lib/Expressions.h rename to wasm/symbols-backend/lib/Expressions.h diff --git a/extensions/cxx_debugging/lib/Variables.cc b/wasm/symbols-backend/lib/Variables.cc similarity index 100% rename from extensions/cxx_debugging/lib/Variables.cc rename to wasm/symbols-backend/lib/Variables.cc diff --git a/extensions/cxx_debugging/lib/Variables.h b/wasm/symbols-backend/lib/Variables.h similarity index 100% rename from extensions/cxx_debugging/lib/Variables.h rename to wasm/symbols-backend/lib/Variables.h diff --git a/extensions/cxx_debugging/lib/WasmModule.cc b/wasm/symbols-backend/lib/WasmModule.cc similarity index 100% rename from extensions/cxx_debugging/lib/WasmModule.cc rename to wasm/symbols-backend/lib/WasmModule.cc diff --git a/extensions/cxx_debugging/lib/WasmModule.h b/wasm/symbols-backend/lib/WasmModule.h similarity index 100% rename from extensions/cxx_debugging/lib/WasmModule.h rename to wasm/symbols-backend/lib/WasmModule.h diff --git a/extensions/cxx_debugging/lib/WasmVendorPlugins.cc b/wasm/symbols-backend/lib/WasmVendorPlugins.cc similarity index 100% rename from extensions/cxx_debugging/lib/WasmVendorPlugins.cc rename to wasm/symbols-backend/lib/WasmVendorPlugins.cc diff --git a/extensions/cxx_debugging/lib/WasmVendorPlugins.h b/wasm/symbols-backend/lib/WasmVendorPlugins.h similarity index 100% rename from extensions/cxx_debugging/lib/WasmVendorPlugins.h rename to wasm/symbols-backend/lib/WasmVendorPlugins.h diff --git a/extensions/cxx_debugging/lib/api.h b/wasm/symbols-backend/lib/api.h similarity index 100% rename from extensions/cxx_debugging/lib/api.h rename to wasm/symbols-backend/lib/api.h diff --git a/extensions/cxx_debugging/src/CMakeLists.txt b/wasm/symbols-backend/src/CMakeLists.txt similarity index 100% rename from extensions/cxx_debugging/src/CMakeLists.txt rename to wasm/symbols-backend/src/CMakeLists.txt diff --git a/extensions/cxx_debugging/src/SymbolsBackend.cc b/wasm/symbols-backend/src/SymbolsBackend.cc similarity index 100% rename from extensions/cxx_debugging/src/SymbolsBackend.cc rename to wasm/symbols-backend/src/SymbolsBackend.cc diff --git a/extensions/cxx_debugging/tests/CMakeLists.txt b/wasm/symbols-backend/tests/CMakeLists.txt similarity index 100% rename from extensions/cxx_debugging/tests/CMakeLists.txt rename to wasm/symbols-backend/tests/CMakeLists.txt diff --git a/extensions/cxx_debugging/tests/LLDBEvalExtensions.h b/wasm/symbols-backend/tests/LLDBEvalExtensions.h similarity index 100% rename from extensions/cxx_debugging/tests/LLDBEvalExtensions.h rename to wasm/symbols-backend/tests/LLDBEvalExtensions.h diff --git a/extensions/cxx_debugging/tests/LLDBEvalTests.d.ts b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts similarity index 100% rename from extensions/cxx_debugging/tests/LLDBEvalTests.d.ts rename to wasm/symbols-backend/tests/LLDBEvalTests.d.ts diff --git a/extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts b/wasm/symbols-backend/tests/SymbolsBackendTests.d.ts similarity index 100% rename from extensions/cxx_debugging/tests/SymbolsBackendTests.d.ts rename to wasm/symbols-backend/tests/SymbolsBackendTests.d.ts diff --git a/extensions/cxx_debugging/tests/WasmModule_test.cc b/wasm/symbols-backend/tests/WasmModule_test.cc similarity index 100% rename from extensions/cxx_debugging/tests/WasmModule_test.cc rename to wasm/symbols-backend/tests/WasmModule_test.cc diff --git a/extensions/cxx_debugging/tests/inputs/CMakeLists.txt b/wasm/symbols-backend/tests/inputs/CMakeLists.txt similarity index 100% rename from extensions/cxx_debugging/tests/inputs/CMakeLists.txt rename to wasm/symbols-backend/tests/inputs/CMakeLists.txt diff --git a/extensions/cxx_debugging/tests/inputs/addr_index.s b/wasm/symbols-backend/tests/inputs/addr_index.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/addr_index.s rename to wasm/symbols-backend/tests/inputs/addr_index.s diff --git a/extensions/cxx_debugging/tests/inputs/addresses.cc b/wasm/symbols-backend/tests/inputs/addresses.cc similarity index 100% rename from extensions/cxx_debugging/tests/inputs/addresses.cc rename to wasm/symbols-backend/tests/inputs/addresses.cc diff --git a/extensions/cxx_debugging/tests/inputs/classstatic.s b/wasm/symbols-backend/tests/inputs/classstatic.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/classstatic.s rename to wasm/symbols-backend/tests/inputs/classstatic.s diff --git a/extensions/cxx_debugging/tests/inputs/dw_opcodes.def b/wasm/symbols-backend/tests/inputs/dw_opcodes.def similarity index 100% rename from extensions/cxx_debugging/tests/inputs/dw_opcodes.def rename to wasm/symbols-backend/tests/inputs/dw_opcodes.def diff --git a/extensions/cxx_debugging/tests/inputs/embedded.s b/wasm/symbols-backend/tests/inputs/embedded.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/embedded.s rename to wasm/symbols-backend/tests/inputs/embedded.s diff --git a/extensions/cxx_debugging/tests/inputs/enums.s b/wasm/symbols-backend/tests/inputs/enums.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/enums.s rename to wasm/symbols-backend/tests/inputs/enums.s diff --git a/extensions/cxx_debugging/tests/inputs/externref.js b/wasm/symbols-backend/tests/inputs/externref.js similarity index 100% rename from extensions/cxx_debugging/tests/inputs/externref.js rename to wasm/symbols-backend/tests/inputs/externref.js diff --git a/extensions/cxx_debugging/tests/inputs/externref.s b/wasm/symbols-backend/tests/inputs/externref.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/externref.s rename to wasm/symbols-backend/tests/inputs/externref.s diff --git a/extensions/cxx_debugging/tests/inputs/globals.s b/wasm/symbols-backend/tests/inputs/globals.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/globals.s rename to wasm/symbols-backend/tests/inputs/globals.s diff --git a/extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s b/wasm/symbols-backend/tests/inputs/hello-split-missing-dwo.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/hello-split-missing-dwo.s rename to wasm/symbols-backend/tests/inputs/hello-split-missing-dwo.s diff --git a/extensions/cxx_debugging/tests/inputs/hello-split.s b/wasm/symbols-backend/tests/inputs/hello-split.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/hello-split.s rename to wasm/symbols-backend/tests/inputs/hello-split.s diff --git a/extensions/cxx_debugging/tests/inputs/hello.s b/wasm/symbols-backend/tests/inputs/hello.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/hello.s rename to wasm/symbols-backend/tests/inputs/hello.s diff --git a/extensions/cxx_debugging/tests/inputs/helper.s b/wasm/symbols-backend/tests/inputs/helper.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/helper.s rename to wasm/symbols-backend/tests/inputs/helper.s diff --git a/extensions/cxx_debugging/tests/inputs/inline.s b/wasm/symbols-backend/tests/inputs/inline.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/inline.s rename to wasm/symbols-backend/tests/inputs/inline.s diff --git a/extensions/cxx_debugging/tests/inputs/namespaces.s b/wasm/symbols-backend/tests/inputs/namespaces.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/namespaces.s rename to wasm/symbols-backend/tests/inputs/namespaces.s diff --git a/extensions/cxx_debugging/tests/inputs/shadowing.s b/wasm/symbols-backend/tests/inputs/shadowing.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/shadowing.s rename to wasm/symbols-backend/tests/inputs/shadowing.s diff --git a/extensions/cxx_debugging/tests/inputs/split-dwarf.s b/wasm/symbols-backend/tests/inputs/split-dwarf.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/split-dwarf.s rename to wasm/symbols-backend/tests/inputs/split-dwarf.s diff --git a/extensions/cxx_debugging/tests/inputs/string_view.cc b/wasm/symbols-backend/tests/inputs/string_view.cc similarity index 100% rename from extensions/cxx_debugging/tests/inputs/string_view.cc rename to wasm/symbols-backend/tests/inputs/string_view.cc diff --git a/extensions/cxx_debugging/tests/inputs/windows_paths.s b/wasm/symbols-backend/tests/inputs/windows_paths.s similarity index 100% rename from extensions/cxx_debugging/tests/inputs/windows_paths.s rename to wasm/symbols-backend/tests/inputs/windows_paths.s diff --git a/extensions/cxx_debugging/third_party/.clang-format b/wasm/symbols-backend/third_party/.clang-format similarity index 100% rename from extensions/cxx_debugging/third_party/.clang-format rename to wasm/symbols-backend/third_party/.clang-format diff --git a/extensions/cxx_debugging/third_party/.gitignore b/wasm/symbols-backend/third_party/.gitignore similarity index 100% rename from extensions/cxx_debugging/third_party/.gitignore rename to wasm/symbols-backend/third_party/.gitignore From 331eba97a6589a1aeab0a2dfc1aacd48a20a91d1 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Thu, 10 Apr 2025 09:06:41 +1000 Subject: [PATCH 3/9] Update reference to LICENSE file of imported code --- src/CustomFormatters.ts | 7 ++--- src/DWARFLanguageExtensionPlugin.ts | 7 ++--- src/ExtensionAPI.d.ts | 7 ++--- src/Formatters.ts | 7 ++--- src/{ModuleConfiguration.ts => PathUtils.ts} | 7 ++--- src/RPCInterface.ts | 7 ++--- ...MFSResourceLoader.ts => ResourceLoader.ts} | 7 ++--- src/SymbolsBackend.d.ts | 7 ++--- src/WasmTypes.ts | 7 ++--- src/WorkerRPC.ts | 7 ++--- test/TestUtils.ts => test-utils/TestValue.ts | 7 ++--- ...tters_test.ts => CustomFormatters.test.ts} | 7 ++--- test/Formatters.test.ts | 7 ++--- .../{Interpreter_test.ts => LLDBEval.test.ts} | 7 ++--- ...onfiguration_test.ts => PathUtils.test.ts} | 7 ++--- ...Backend_test.ts => SymbolsBackend.test.ts} | 7 ++--- wasm/e2e/cpp_eval.yaml | 7 ++--- wasm/e2e/pointer.yaml | 7 ++--- wasm/e2e/resources/huge-source-file.cc | 2 +- wasm/e2e/resources/pointers.cc | 2 +- .../e2e/resources/scope-view-non-primitives.c | 2 +- .../resources/scope-view-non-primitives.cpp | 2 +- wasm/e2e/resources/scope-view-primitives.c | 2 +- wasm/e2e/resources/stepping-with-state.c | 2 +- wasm/e2e/resources/test_wasm_simd.c | 2 +- wasm/e2e/resources/vector.cc | 2 +- wasm/e2e/resources/wchar.cc | 2 +- wasm/e2e/scope_view_non_primitives.yaml | 7 ++--- wasm/e2e/scope_view_non_primitives_cpp.yaml | 7 ++--- wasm/e2e/scope_view_primitives.yaml | 7 ++--- wasm/e2e/stepping_with_state.yaml | 7 ++--- wasm/e2e/string_view.yaml | 7 ++--- wasm/e2e/test_big_dwo.yaml | 7 ++--- wasm/e2e/test_loop.yaml | 7 ++--- wasm/e2e/test_wasm_simd.yaml | 7 ++--- wasm/e2e/vector.yaml | 7 ++--- wasm/e2e/wchar.yaml | 7 ++--- wasm/symbols-backend/LICENSE | 27 +++++++++++++++++++ wasm/symbols-backend/README.md | 3 +++ wasm/symbols-backend/lib/ApiContext.cc | 2 +- wasm/symbols-backend/lib/ApiContext.h | 2 +- wasm/symbols-backend/lib/Expressions.cc | 2 +- wasm/symbols-backend/lib/Expressions.h | 2 +- wasm/symbols-backend/lib/Variables.cc | 2 +- wasm/symbols-backend/lib/Variables.h | 2 +- wasm/symbols-backend/lib/WasmModule.cc | 2 +- wasm/symbols-backend/lib/WasmModule.h | 2 +- wasm/symbols-backend/lib/WasmVendorPlugins.cc | 2 +- wasm/symbols-backend/lib/WasmVendorPlugins.h | 2 +- wasm/symbols-backend/lib/api.h | 2 +- wasm/symbols-backend/src/SymbolsBackend.cc | 2 +- .../tests/LLDBEvalExtensions.h | 2 +- wasm/symbols-backend/tests/LLDBEvalTests.d.ts | 2 +- .../tests/SymbolsBackendTests.d.ts | 2 +- wasm/symbols-backend/tests/WasmModule_test.cc | 2 +- .../symbols-backend/tests/inputs/addresses.cc | 2 +- .../tests/inputs/string_view.cc | 2 +- 57 files changed, 169 insertions(+), 111 deletions(-) rename src/{ModuleConfiguration.ts => PathUtils.ts} (93%) rename src/{MEMFSResourceLoader.ts => ResourceLoader.ts} (93%) rename test/TestUtils.ts => test-utils/TestValue.ts (96%) rename test/{CustomFormatters_test.ts => CustomFormatters.test.ts} (97%) rename test/{Interpreter_test.ts => LLDBEval.test.ts} (96%) rename test/{ModuleConfiguration_test.ts => PathUtils.test.ts} (96%) rename test/{SymbolsBackend_test.ts => SymbolsBackend.test.ts} (80%) create mode 100644 wasm/symbols-backend/LICENSE create mode 100644 wasm/symbols-backend/README.md diff --git a/src/CustomFormatters.ts b/src/CustomFormatters.ts index 7c6ee1d..e335af2 100644 --- a/src/CustomFormatters.ts +++ b/src/CustomFormatters.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/CustomFormatters.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; diff --git a/src/DWARFLanguageExtensionPlugin.ts b/src/DWARFLanguageExtensionPlugin.ts index 01ab958..6546fb8 100644 --- a/src/DWARFLanguageExtensionPlugin.ts +++ b/src/DWARFLanguageExtensionPlugin.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/DWARFSymbols.ts import './Formatters.js'; diff --git a/src/ExtensionAPI.d.ts b/src/ExtensionAPI.d.ts index c3ad135..1ac4212 100644 --- a/src/ExtensionAPI.d.ts +++ b/src/ExtensionAPI.d.ts @@ -1,6 +1,7 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extension-api/ExtensionAPI.d.ts export namespace Chrome { export namespace DevTools { diff --git a/src/Formatters.ts b/src/Formatters.ts index 9ce1d87..d4e1b74 100644 --- a/src/Formatters.ts +++ b/src/Formatters.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/Formatters.ts import { CustomFormatters, diff --git a/src/ModuleConfiguration.ts b/src/PathUtils.ts similarity index 93% rename from src/ModuleConfiguration.ts rename to src/PathUtils.ts index f4f0e8c..c9aef82 100644 --- a/src/ModuleConfiguration.ts +++ b/src/PathUtils.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/ModuleConfiguration.ts import {globMatch} from './GlobMatch.js'; diff --git a/src/RPCInterface.ts b/src/RPCInterface.ts index 92511ba..35b2769 100644 --- a/src/RPCInterface.ts +++ b/src/RPCInterface.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/DevToolsPluginWorker.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; diff --git a/src/MEMFSResourceLoader.ts b/src/ResourceLoader.ts similarity index 93% rename from src/MEMFSResourceLoader.ts rename to src/ResourceLoader.ts index d103425..2cc73b9 100644 --- a/src/MEMFSResourceLoader.ts +++ b/src/ResourceLoader.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/MEMFSResourceLoader.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; diff --git a/src/SymbolsBackend.d.ts b/src/SymbolsBackend.d.ts index 1cfc747..8c3240b 100644 --- a/src/SymbolsBackend.d.ts +++ b/src/SymbolsBackend.d.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/SymbolsBackend.d.ts /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/src/WasmTypes.ts b/src/WasmTypes.ts index 499b78d..ea60f27 100644 --- a/src/WasmTypes.ts +++ b/src/WasmTypes.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/WasmTypes.ts import type {Chrome} from '../../../extension-api/ExtensionAPI'; diff --git a/src/WorkerRPC.ts b/src/WorkerRPC.ts index 4961e01..1e74ffe 100644 --- a/src/WorkerRPC.ts +++ b/src/WorkerRPC.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/WorkerRPC.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; diff --git a/test/TestUtils.ts b/test-utils/TestValue.ts similarity index 96% rename from test/TestUtils.ts rename to test-utils/TestValue.ts index c5f2e03..9964d7a 100644 --- a/test/TestUtils.ts +++ b/test-utils/TestValue.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/TestUtils.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; import type {Value, WasmInterface} from '../src/CustomFormatters.js'; diff --git a/test/CustomFormatters_test.ts b/test/CustomFormatters.test.ts similarity index 97% rename from test/CustomFormatters_test.ts rename to test/CustomFormatters.test.ts index c9bb1e5..d57be6f 100644 --- a/test/CustomFormatters_test.ts +++ b/test/CustomFormatters.test.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/CustomFormatters_test.ts import {MemorySlice, PageStore} from '../src/CustomFormatters.js'; diff --git a/test/Formatters.test.ts b/test/Formatters.test.ts index b49a816..d6e8037 100644 --- a/test/Formatters.test.ts +++ b/test/Formatters.test.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Formatters_test.ts import {CustomFormatters, type TypeInfo} from '../src/CustomFormatters.js'; import * as Formatters from '../src/Formatters.js'; diff --git a/test/Interpreter_test.ts b/test/LLDBEval.test.ts similarity index 96% rename from test/Interpreter_test.ts rename to test/LLDBEval.test.ts index 4efe7c9..5f1d9f3 100644 --- a/test/Interpreter_test.ts +++ b/test/LLDBEval.test.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Interpreter_test.ts import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; import {createEmbindPool} from '../src/DWARFSymbols.js'; diff --git a/test/ModuleConfiguration_test.ts b/test/PathUtils.test.ts similarity index 96% rename from test/ModuleConfiguration_test.ts rename to test/PathUtils.test.ts index 7a72d7b..f525337 100644 --- a/test/ModuleConfiguration_test.ts +++ b/test/PathUtils.test.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts import {findModuleConfiguration, resolveSourcePathToURL} from '../src/ModuleConfiguration.js'; diff --git a/test/SymbolsBackend_test.ts b/test/SymbolsBackend.test.ts similarity index 80% rename from test/SymbolsBackend_test.ts rename to test/SymbolsBackend.test.ts index 57941e3..cf98933 100644 --- a/test/SymbolsBackend_test.ts +++ b/test/SymbolsBackend.test.ts @@ -1,6 +1,7 @@ -// Copyright 2023 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/SymbolsBackend_test.ts import createModule, {type SymbolsBackendTestsModule} from './SymbolsBackendTests.js'; diff --git a/wasm/e2e/cpp_eval.yaml b/wasm/e2e/cpp_eval.yaml index f95a999..321dcf8 100644 --- a/wasm/e2e/cpp_eval.yaml +++ b/wasm/e2e/cpp_eval.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml name: Cpp expression evaluation source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c diff --git a/wasm/e2e/pointer.yaml b/wasm/e2e/pointer.yaml index e060dd5..6b5f3ab 100644 --- a/wasm/e2e/pointer.yaml +++ b/wasm/e2e/pointer.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/pointer.yaml name: Test Pointer Chains source_file: //extensions/cxx_debugging/e2e/resources/pointers.cc diff --git a/wasm/e2e/resources/huge-source-file.cc b/wasm/e2e/resources/huge-source-file.cc index ab1874e..4b8e55e 100644 --- a/wasm/e2e/resources/huge-source-file.cc +++ b/wasm/e2e/resources/huge-source-file.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include diff --git a/wasm/e2e/resources/pointers.cc b/wasm/e2e/resources/pointers.cc index c0021c2..dc83f6c 100644 --- a/wasm/e2e/resources/pointers.cc +++ b/wasm/e2e/resources/pointers.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include diff --git a/wasm/e2e/resources/scope-view-non-primitives.c b/wasm/e2e/resources/scope-view-non-primitives.c index d5d4beb..31da28e 100644 --- a/wasm/e2e/resources/scope-view-non-primitives.c +++ b/wasm/e2e/resources/scope-view-non-primitives.c @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/e2e/resources/scope-view-non-primitives.cpp b/wasm/e2e/resources/scope-view-non-primitives.cpp index ad2c00b..f2ba339 100644 --- a/wasm/e2e/resources/scope-view-non-primitives.cpp +++ b/wasm/e2e/resources/scope-view-non-primitives.cpp @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/e2e/resources/scope-view-primitives.c b/wasm/e2e/resources/scope-view-primitives.c index 37cb21a..4e7dcc8 100644 --- a/wasm/e2e/resources/scope-view-primitives.c +++ b/wasm/e2e/resources/scope-view-primitives.c @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/e2e/resources/stepping-with-state.c b/wasm/e2e/resources/stepping-with-state.c index 2db4975..2c5b635 100644 --- a/wasm/e2e/resources/stepping-with-state.c +++ b/wasm/e2e/resources/stepping-with-state.c @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include diff --git a/wasm/e2e/resources/test_wasm_simd.c b/wasm/e2e/resources/test_wasm_simd.c index a50268a..e85933a 100644 --- a/wasm/e2e/resources/test_wasm_simd.c +++ b/wasm/e2e/resources/test_wasm_simd.c @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include diff --git a/wasm/e2e/resources/vector.cc b/wasm/e2e/resources/vector.cc index e1c1430..4e51478 100644 --- a/wasm/e2e/resources/vector.cc +++ b/wasm/e2e/resources/vector.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/e2e/resources/wchar.cc b/wasm/e2e/resources/wchar.cc index 2d9a17a..abdd9e8 100644 --- a/wasm/e2e/resources/wchar.cc +++ b/wasm/e2e/resources/wchar.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/e2e/scope_view_non_primitives.yaml b/wasm/e2e/scope_view_non_primitives.yaml index f7c1714..2475135 100644 --- a/wasm/e2e/scope_view_non_primitives.yaml +++ b/wasm/e2e/scope_view_non_primitives.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives.yaml name: Scope view formats non primitive types correctly # This test checks that the scope view displays diff --git a/wasm/e2e/scope_view_non_primitives_cpp.yaml b/wasm/e2e/scope_view_non_primitives_cpp.yaml index 58cbc95..ac88a1e 100644 --- a/wasm/e2e/scope_view_non_primitives_cpp.yaml +++ b/wasm/e2e/scope_view_non_primitives_cpp.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/scope_view_non_primitives_cpp.yaml name: Scope view formats non primitive types correctly for cpp # This test checks that the scope view displays diff --git a/wasm/e2e/scope_view_primitives.yaml b/wasm/e2e/scope_view_primitives.yaml index 1211fb2..d678d18 100644 --- a/wasm/e2e/scope_view_primitives.yaml +++ b/wasm/e2e/scope_view_primitives.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/scope_view_primitives.yaml name: Scope view formats primitive types correctly # This test checks that the scope view displays diff --git a/wasm/e2e/stepping_with_state.yaml b/wasm/e2e/stepping_with_state.yaml index 1a4d154..c0360fb 100644 --- a/wasm/e2e/stepping_with_state.yaml +++ b/wasm/e2e/stepping_with_state.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml name: Stepping with state source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c diff --git a/wasm/e2e/string_view.yaml b/wasm/e2e/string_view.yaml index f697b2c..473034b 100644 --- a/wasm/e2e/string_view.yaml +++ b/wasm/e2e/string_view.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/string_view.yaml name: Test String View source_file: //extensions/cxx_debugging/tests/inputs/string_view.cc diff --git a/wasm/e2e/test_big_dwo.yaml b/wasm/e2e/test_big_dwo.yaml index 031917e..e71dd60 100644 --- a/wasm/e2e/test_big_dwo.yaml +++ b/wasm/e2e/test_big_dwo.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml name: Successfully loads large dwo files source_file: //extensions/cxx_debugging/e2e/resources/huge-source-file.cc diff --git a/wasm/e2e/test_loop.yaml b/wasm/e2e/test_loop.yaml index 01e6feb..5072d9a 100644 --- a/wasm/e2e/test_loop.yaml +++ b/wasm/e2e/test_loop.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_loop.yaml name: test_loop source_file: //third_party/emscripten-releases/install/emscripten/tests/core/test_loop.c diff --git a/wasm/e2e/test_wasm_simd.yaml b/wasm/e2e/test_wasm_simd.yaml index 2b18eac..d4abf74 100644 --- a/wasm/e2e/test_wasm_simd.yaml +++ b/wasm/e2e/test_wasm_simd.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml name: test_wasm_simd source_file: //extensions/cxx_debugging/e2e/resources/test_wasm_simd.c diff --git a/wasm/e2e/vector.yaml b/wasm/e2e/vector.yaml index d7267fb..70233a7 100644 --- a/wasm/e2e/vector.yaml +++ b/wasm/e2e/vector.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/vector.yaml name: Test std::vector source_file: //extensions/cxx_debugging/e2e/resources/vector.cc diff --git a/wasm/e2e/wchar.yaml b/wasm/e2e/wchar.yaml index 1aa24af..4a850f8 100644 --- a/wasm/e2e/wchar.yaml +++ b/wasm/e2e/wchar.yaml @@ -1,6 +1,7 @@ -# Copyright 2023 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. +# This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +# of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +# +# https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/wchar.yaml name: Test Wide-Character Strings source_file: //extensions/cxx_debugging/e2e/resources/wchar.cc diff --git a/wasm/symbols-backend/LICENSE b/wasm/symbols-backend/LICENSE new file mode 100644 index 0000000..972bb2e --- /dev/null +++ b/wasm/symbols-backend/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wasm/symbols-backend/README.md b/wasm/symbols-backend/README.md new file mode 100644 index 0000000..ef1e8ad --- /dev/null +++ b/wasm/symbols-backend/README.md @@ -0,0 +1,3 @@ +# SymbolsBackend.wasm + +This is a modified copy of revision `cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1` of the [Chrome DevTools C/C++ Debugging Extension](https://github.com/ChromeDevTools/devtools-frontend/tree/main/extensions/cxx_debugging) code. \ No newline at end of file diff --git a/wasm/symbols-backend/lib/ApiContext.cc b/wasm/symbols-backend/lib/ApiContext.cc index 23fdeac..b024fa5 100644 --- a/wasm/symbols-backend/lib/ApiContext.cc +++ b/wasm/symbols-backend/lib/ApiContext.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "ApiContext.h" #include "Expressions.h" diff --git a/wasm/symbols-backend/lib/ApiContext.h b/wasm/symbols-backend/lib/ApiContext.h index 5664135..2643e63 100644 --- a/wasm/symbols-backend/lib/ApiContext.h +++ b/wasm/symbols-backend/lib/ApiContext.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #ifndef EXTENSIONS_CXX_DEBUGGING_API_CONTEXT_H_ #define EXTENSIONS_CXX_DEBUGGING_API_CONTEXT_H_ diff --git a/wasm/symbols-backend/lib/Expressions.cc b/wasm/symbols-backend/lib/Expressions.cc index 97edafe..3b7e9b7 100644 --- a/wasm/symbols-backend/lib/Expressions.cc +++ b/wasm/symbols-backend/lib/Expressions.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "Expressions.h" #include "ApiContext.h" diff --git a/wasm/symbols-backend/lib/Expressions.h b/wasm/symbols-backend/lib/Expressions.h index 395d5d6..1053d30 100644 --- a/wasm/symbols-backend/lib/Expressions.h +++ b/wasm/symbols-backend/lib/Expressions.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #ifndef EXTENSIONS_CXX_DEBUGGING_EXPRESSIONS_H_ #define EXTENSIONS_CXX_DEBUGGING_EXPRESSIONS_H_ diff --git a/wasm/symbols-backend/lib/Variables.cc b/wasm/symbols-backend/lib/Variables.cc index 3da7ea3..874e1aa 100644 --- a/wasm/symbols-backend/lib/Variables.cc +++ b/wasm/symbols-backend/lib/Variables.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "Variables.h" diff --git a/wasm/symbols-backend/lib/Variables.h b/wasm/symbols-backend/lib/Variables.h index 218582a..f2db1e5 100644 --- a/wasm/symbols-backend/lib/Variables.h +++ b/wasm/symbols-backend/lib/Variables.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #ifndef EXTENSIONS_CXX_DEBUGGING_VARIABLES_H_ #define EXTENSIONS_CXX_DEBUGGING_VARIABLES_H_ diff --git a/wasm/symbols-backend/lib/WasmModule.cc b/wasm/symbols-backend/lib/WasmModule.cc index 07c4b53..c97703a 100644 --- a/wasm/symbols-backend/lib/WasmModule.cc +++ b/wasm/symbols-backend/lib/WasmModule.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "WasmModule.h" #include "Expressions.h" diff --git a/wasm/symbols-backend/lib/WasmModule.h b/wasm/symbols-backend/lib/WasmModule.h index 2ea188c..fc26809 100644 --- a/wasm/symbols-backend/lib/WasmModule.h +++ b/wasm/symbols-backend/lib/WasmModule.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #ifndef EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ #define EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ diff --git a/wasm/symbols-backend/lib/WasmVendorPlugins.cc b/wasm/symbols-backend/lib/WasmVendorPlugins.cc index 7af53b5..b0b7b76 100644 --- a/wasm/symbols-backend/lib/WasmVendorPlugins.cc +++ b/wasm/symbols-backend/lib/WasmVendorPlugins.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "WasmVendorPlugins.h" diff --git a/wasm/symbols-backend/lib/WasmVendorPlugins.h b/wasm/symbols-backend/lib/WasmVendorPlugins.h index 8f2829b..00348af 100644 --- a/wasm/symbols-backend/lib/WasmVendorPlugins.h +++ b/wasm/symbols-backend/lib/WasmVendorPlugins.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #ifndef EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ #define EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ diff --git a/wasm/symbols-backend/lib/api.h b/wasm/symbols-backend/lib/api.h index 80ba820..5265b20 100644 --- a/wasm/symbols-backend/lib/api.h +++ b/wasm/symbols-backend/lib/api.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. // THIS IS GENERATED CODE, DO NOT MODIFY! // clang-format off diff --git a/wasm/symbols-backend/src/SymbolsBackend.cc b/wasm/symbols-backend/src/SymbolsBackend.cc index cb0763d..2ffe6ac 100644 --- a/wasm/symbols-backend/src/SymbolsBackend.cc +++ b/wasm/symbols-backend/src/SymbolsBackend.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. // clang-format off #include diff --git a/wasm/symbols-backend/tests/LLDBEvalExtensions.h b/wasm/symbols-backend/tests/LLDBEvalExtensions.h index fcbf2ad..820bd21 100644 --- a/wasm/symbols-backend/tests/LLDBEvalExtensions.h +++ b/wasm/symbols-backend/tests/LLDBEvalExtensions.h @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include diff --git a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts index cea4d69..fa810ba 100644 --- a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts +++ b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. import type {Vector} from '../src/SymbolsBackend.js'; export interface Debugger { diff --git a/wasm/symbols-backend/tests/SymbolsBackendTests.d.ts b/wasm/symbols-backend/tests/SymbolsBackendTests.d.ts index ce6f559..afbf530 100644 --- a/wasm/symbols-backend/tests/SymbolsBackendTests.d.ts +++ b/wasm/symbols-backend/tests/SymbolsBackendTests.d.ts @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. export interface SymbolsBackendTestsModule extends EmscriptenModule { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/wasm/symbols-backend/tests/WasmModule_test.cc b/wasm/symbols-backend/tests/WasmModule_test.cc index 2c6a7b1..5899fa9 100644 --- a/wasm/symbols-backend/tests/WasmModule_test.cc +++ b/wasm/symbols-backend/tests/WasmModule_test.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include "WasmModule.h" #include "WasmVendorPlugins.h" diff --git a/wasm/symbols-backend/tests/inputs/addresses.cc b/wasm/symbols-backend/tests/inputs/addresses.cc index a07a481..b74c553 100644 --- a/wasm/symbols-backend/tests/inputs/addresses.cc +++ b/wasm/symbols-backend/tests/inputs/addresses.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include diff --git a/wasm/symbols-backend/tests/inputs/string_view.cc b/wasm/symbols-backend/tests/inputs/string_view.cc index 5277c1a..1564298 100644 --- a/wasm/symbols-backend/tests/inputs/string_view.cc +++ b/wasm/symbols-backend/tests/inputs/string_view.cc @@ -1,6 +1,6 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// found in the wasm/symbols-backend/LICENSE file. #include #include From 120a00400106ff3674d12ba8b2d69356aa7278f2 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Thu, 10 Apr 2025 09:23:43 +1000 Subject: [PATCH 4/9] Update build scripts to use the imported source and refactor source code and tests accordingly - Build scripts for stage 1 and 2 of SymbolsBackend - Build scripts for e2e resources - All TypeScript compilation done outside of CMake - Test runner for e2e spec files - Enhance formatting of strings (VSCode debugger expects string to be quoted) - Fix loading of dwo files (was broken when not running in browser worker thread) --- .gitignore | 10 +- .npmignore | 5 +- .vscode/settings.json | 26 +- README.md | 2 +- chrome-cxx/Dockerfile | 29 - chrome-cxx/mnt/do-copy.sh | 20 - esbuild.js | 25 - package-lock.json | 671 ++---------------- package.json | 28 +- pipeline.yml | 5 +- src/CustomFormatters.ts | 141 ++-- src/DWARFLanguageExtensionPlugin.ts | 261 ++++--- src/ExtensionAPI.d.ts | 226 +----- src/Formatters.ts | 130 ++-- src/PathUtils.ts | 90 +-- src/RPCInterface.ts | 36 +- src/ResourceLoader.ts | 61 +- src/SymbolsBackend.d.ts | 38 +- src/WasmTypes.ts | 23 +- src/WorkerRPC.ts | 61 +- src/index.ts | 18 +- src/inject.ts | 3 - src/worker.ts | 33 +- test-utils/DebuggerSession.ts | 527 ++++++++++++++ test-utils/LLDBEvalDebugger.ts | 77 ++ test-utils/TestValue.ts | 128 +--- test-utils/TestWasmInterface.ts | 39 + .../emscripten-module-runner/child-process.js | 8 + test-utils/emscripten-module-runner/index.ts | 48 ++ test-utils/sync-interface/index.ts | 43 ++ test-utils/sync-interface/worker.js | 49 ++ test/CustomFormatters.test.ts | 235 +++--- test/Formatters.test.ts | 193 +++-- test/LLDBEval.test.ts | 220 ------ test/PathUtils.test.ts | 184 ++--- test/SymbolsBackend.test.ts | 54 +- ...{mandelbrot.test.ts => WasmWorker.test.ts} | 20 +- test/e2e/e2e-specs.test.ts | 245 +++++++ test/e2e/lldb-eval.test.ts | 101 +++ test/{ => resources}/mandelbrot.wasm | Bin tsconfig.json | 22 +- wasm/build-e2e.sh | 13 + wasm/build-stage-1.sh | 28 + wasm/build-stage-2.sh | 43 ++ wasm/build.sh | 40 ++ wasm/builder/Dockerfile | 24 + wasm/e2e/cpp_eval.yaml | 6 +- wasm/e2e/pointer.yaml | 6 +- wasm/e2e/resources/stepping-with-state.c | 2 +- wasm/e2e/scope_view_non_primitives.yaml | 2 +- wasm/e2e/scope_view_non_primitives_cpp.yaml | 4 +- wasm/e2e/scope_view_primitives.yaml | 4 +- wasm/e2e/stepping_with_state.yaml | 12 +- wasm/e2e/string_view.yaml | 4 +- wasm/e2e/test_big_dwo.yaml | 4 +- wasm/e2e/test_loop.yaml | 2 +- wasm/e2e/test_wasm_simd.yaml | 2 +- wasm/e2e/vector.yaml | 2 +- wasm/e2e/wchar.yaml | 16 +- wasm/fetch-git.sh | 34 + wasm/generate-e2e-resources-build | 71 ++ wasm/symbols-backend/CMakeLists.txt | 81 +-- wasm/symbols-backend/src/CMakeLists.txt | 68 +- wasm/symbols-backend/tests/CMakeLists.txt | 34 +- .../tests/LLDBEvalExtensions.h | 9 +- wasm/symbols-backend/tests/LLDBEvalTests.d.ts | 14 +- .../tests/inputs/CMakeLists.txt | 19 +- 67 files changed, 2387 insertions(+), 2292 deletions(-) delete mode 100644 chrome-cxx/Dockerfile delete mode 100644 chrome-cxx/mnt/do-copy.sh delete mode 100644 esbuild.js delete mode 100644 src/inject.ts create mode 100644 test-utils/DebuggerSession.ts create mode 100644 test-utils/LLDBEvalDebugger.ts create mode 100644 test-utils/TestWasmInterface.ts create mode 100644 test-utils/emscripten-module-runner/child-process.js create mode 100644 test-utils/emscripten-module-runner/index.ts create mode 100644 test-utils/sync-interface/index.ts create mode 100644 test-utils/sync-interface/worker.js delete mode 100644 test/LLDBEval.test.ts rename test/{mandelbrot.test.ts => WasmWorker.test.ts} (75%) create mode 100644 test/e2e/e2e-specs.test.ts create mode 100644 test/e2e/lldb-eval.test.ts rename test/{ => resources}/mandelbrot.wasm (100%) create mode 100644 wasm/build-e2e.sh create mode 100755 wasm/build-stage-1.sh create mode 100755 wasm/build-stage-2.sh create mode 100755 wasm/build.sh create mode 100644 wasm/builder/Dockerfile create mode 100644 wasm/fetch-git.sh create mode 100755 wasm/generate-e2e-resources-build diff --git a/.gitignore b/.gitignore index 0cf105f..be1af34 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,10 @@ dist .yarn/install-state.gz .pnp.* -/chrome-cxx/mnt/extension -/chrome-cxx/mnt/extension-api.d.ts -/dist \ No newline at end of file +/dist +/wasm/e2e.build/ +/wasm/symbols-backend.build/ + +/chrome-cxx + +compile_commands.json \ No newline at end of file diff --git a/.npmignore b/.npmignore index ef8081b..6711127 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,5 @@ /* !/dist +/dist/**/*.js.map -/chrome-cxx/** -!/chrome-cxx/mnt/extension-api.d.ts -!/chrome-cxx/mnt/extension/*.d.ts +!/chrome-cxx diff --git a/.vscode/settings.json b/.vscode/settings.json index 3902f7b..97391d2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,27 @@ { + "editor.tabSize": 2, "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, - "nodejs-testing.extensions": [ - { "extensions": ["ts"], "parameters": ["--loader", "tsx"] } - ] -} + { + "extensions": [ + "ts" + ], + "parameters": [ + "--import", + "tsx" + ] + } + ], + "clangd.arguments": [ + "--query-driver=**/emcc,**/em++,**/clang,**/clang++" + ], + "[cpp]": { + "editor.formatOnSave": false + }, + "[yaml]": { + "editor.formatOnSave": false + } +} \ No newline at end of file diff --git a/README.md b/README.md index c71dc7c..4671724 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This repository publishes an npm module which is used by [js-debug](https://gith - For VS Code users, you will be prompted to install the appropriate extension that contains this module when you first step into a WebAssembly file. - For users of the js-debug standalone DAP server, you can install the `@vscode/dwarf-debugging` alongside js-debug or somewhere in your [NODE_PATH](https://nodejs.org/api/modules.html#loading-from-the-global-folders). -This project works by compiling the Chrome [C/C++ Debugging Extension](https://github.com/ChromeDevTools/devtools-frontend/tree/main/extensions/cxx_debugging) in a way that is usable by Node.js programs. Fortunately, the extension is architected such that most work is done inside a WebWorker, and making this instead run in a Node worker_thread is not terribly difficult. Appropriate TypeScript types are exposed, and this module is then shimmed into js-debug which 'pretends' to be Devtools. +This project works by compiling the WebAssembly backend for the Chrome [C/C++ Debugging Extension](https://github.com/ChromeDevTools/devtools-frontend/tree/main/extensions/cxx_debugging) in a way that is usable by Node.js programs. Fortunately, the extension is architected such that most work is done inside a WebWorker, and porting the TypeScript code to instead run in a Node worker_thread is not terribly difficult. Appropriate TypeScript types are exposed, and this module is then shimmed into js-debug which 'pretends' to be Devtools. ## Contributing diff --git a/chrome-cxx/Dockerfile b/chrome-cxx/Dockerfile deleted file mode 100644 index 4c8bfca..0000000 --- a/chrome-cxx/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM node:20 - -RUN apt update -RUN apt install -y git python3 curl xz-utils build-essential - -RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /depot_tools -ENV PATH="/depot_tools:${PATH}" -RUN update_depot_tools - -RUN mkdir /devtools -ENV TAR_OPTIONS="--no-same-owner --no-same-permissions" - -# It uses Python's 'tarfile' module to extract which doesn't correctly honor all TAR_OPTIONS and forgets file permissions. -# It *should* use the umask with these TAR_OPTIONS, but it does not, and fails to set +x on the node binary. I tried to -# find a better way to deal with this, but didn't get anywhere. It'll try to continue when we sync later. -RUN cd /devtools && fetch --nohistory devtools-frontend || echo "First failed, chmodding and trying again..." -RUN chmod -R 775 /devtools/devtools-frontend/third_party/node - -# Tell gclient to checkout the deps necessary to build the CXX debugging extension -RUN sed -i 's/"custom_deps": {}/"custom_deps": {},"custom_vars":{"checkout_cxx_debugging_extension_deps":True}/' /devtools/.gclient -WORKDIR /devtools/devtools-frontend -RUN gclient sync - -# CXX debugging builds a devtools extension, which is browser-based. But we're running this in Node. Update the -# environment for emscripten so it creates the correct bindings. -RUN sed -i 's/ENVIRONMENT=web,worker/ENVIRONMENT=node/' /devtools/devtools-frontend/extensions/cxx_debugging/src/CMakeLists.txt - -WORKDIR /devtools/devtools-frontend/extensions/cxx_debugging -RUN ./tools/bootstrap.py -no-check -no-goma ../../out \ No newline at end of file diff --git a/chrome-cxx/mnt/do-copy.sh b/chrome-cxx/mnt/do-copy.sh deleted file mode 100644 index 39279e8..0000000 --- a/chrome-cxx/mnt/do-copy.sh +++ /dev/null @@ -1,20 +0,0 @@ -rm -rf /mnt/extension /mnt/extension-api.d.ts /mnt/types - -# debug wasm is ~1GB, no need to copy it... -rm /devtools/devtools-frontend/out/DevTools_CXX_Debugging.stage2/src/*.debug.wasm - -# todo: compressing with brotli saves ~37% from a normal zip, but it would require extraction at -# runtime and self-modifying modules/extensions is spooky, yo - -# echo "Compressing wasm" -# apt install -y brotli -# brotli -Z /devtools/devtools-frontend/out/DevTools_CXX_Debugging.stage2/src/SymbolsBackend.wasm - -echo "Copying base data" -cp -r /devtools/devtools-frontend/out/DevTools_CXX_Debugging.stage2/src /mnt/extension -cp /devtools/devtools-frontend/extension-api/ExtensionAPI.d.ts /mnt/extension-api.d.ts - -echo "Building types" -npx -y --package=typescript -- tsc -p . --declaration --emitDeclarationOnly --outDir /tmp/types -find /tmp/types -type f -exec sed -i 's=../../../extension-api/ExtensionAPI.js=../extension-api.js=g' {} \; -mv /tmp/types/src/* /mnt/extension diff --git a/esbuild.js b/esbuild.js deleted file mode 100644 index dca1f98..0000000 --- a/esbuild.js +++ /dev/null @@ -1,25 +0,0 @@ -const esbuild = require("esbuild"); -const { promises: fs } = require("fs"); - -(async () => { - await fs.mkdir("dist", { recursive: true }); - await Promise.all([ - fs.copyFile( - "chrome-cxx/mnt/extension/SymbolsBackend.wasm", - "dist/SymbolsBackend.wasm" - ), - - esbuild.build({ - entryPoints: ["src/index.ts", "src/worker.ts"], - bundle: true, - platform: "node", - target: ["node18"], - outdir: "dist", - external: ["ws"], - inject: ["src/inject.ts"], - define: { - "import.meta.url": "import_meta_url", - }, - }), - ]); -})(); diff --git a/package-lock.json b/package-lock.json index 0e75853..54a0052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,498 +1,26 @@ { "name": "@vscode/dwarf-debugging", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vscode/dwarf-debugging", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "ws": "^8.14.1" }, "devDependencies": { - "@chialab/esbuild-plugin-meta-url": "^0.17.7", - "@types/emscripten": "^1.39.7", + "@types/emscripten": "1.39.4", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.6.0", - "esbuild": "^0.25.0", + "@types/ws": "^8.14.1", + "devtools-protocol": "^0.0.1436416", + "js-yaml": "^4.1.0", "rimraf": "^5.0.1", "tsx": "^3.12.10", - "typescript": "^5.2.2" - } - }, - "node_modules/@chialab/esbuild-plugin-meta-url": { - "version": "0.17.7", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-meta-url/-/esbuild-plugin-meta-url-0.17.7.tgz", - "integrity": "sha512-f4Eg/+XSrcf9JlYUjI8IEupMOuGR7P3hMlfdbE8aMHM0+PJSIYeFV+udMZyNmSpCYfO3dpVtdulv5QDZfTvzWg==", - "dev": true, - "dependencies": { - "@chialab/esbuild-rna": "^0.17.8", - "@chialab/estransform": "^0.17.4", - "@chialab/node-resolve": "^0.17.1", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">=13" - } - }, - "node_modules/@chialab/esbuild-rna": { - "version": "0.17.8", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.17.8.tgz", - "integrity": "sha512-hovU4W5zlyMnbJjexdczpQ9mcUfFsJuv9FWUhzpXiQwPprlp5lul+frTed9s8AyVDTDq2yq3Gx2Ac411QsXYGA==", - "dev": true, - "dependencies": { - "@chialab/estransform": "^0.17.4", - "@chialab/node-resolve": "^0.17.1" - }, - "engines": { - "node": ">=13" - } - }, - "node_modules/@chialab/estransform": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.17.5.tgz", - "integrity": "sha512-maJUFkwk0ie0L4VvDO74NDYyRvaTQAI0qmSmrms8bZxUkZ+zQZd1ByWKDCYTRwtR6AOzTvgEOl2ZvEG+OUKv/A==", - "dev": true, - "dependencies": { - "@parcel/source-map": "^2.0.0" - }, - "engines": { - "node": ">=13" - } - }, - "node_modules/@chialab/node-resolve": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.17.1.tgz", - "integrity": "sha512-YWaK0MKKeB0FILI6j7qiAlGoSC9MqJZDFXzlAgfOaMCbn8Feqh6njxv7mI3oVkdi7QwV6zPRaTN6hKig/NriMA==", - "dev": true, - "engines": { - "node": ">=13" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "typescript": "^5.8.2" } }, "node_modules/@isaacs/cliui": { @@ -512,18 +40,6 @@ "node": ">=12" } }, - "node_modules/@parcel/source-map": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", - "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", - "dev": true, - "dependencies": { - "detect-libc": "^1.0.3" - }, - "engines": { - "node": "^12.18.3 || >=14" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -535,17 +51,35 @@ } }, "node_modules/@types/emscripten": { - "version": "1.39.7", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.7.tgz", - "integrity": "sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==", + "version": "1.39.4", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.4.tgz", + "integrity": "sha512-k3LLVMFrdNA9UCvMDPWMbFrGPNb+GcPyw29ktJTo1RCN7RmxFG5XzPZcPKRlnLuLT/FRm8wp4ohvDwNY7GlROQ==", "dev": true }, - "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true }, + "node_modules/@types/node": { + "version": "20.17.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -570,6 +104,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -589,8 +129,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/color-convert": { "version": "2.0.1", @@ -624,17 +163,11 @@ "node": ">= 8" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } + "node_modules/devtools-protocol": { + "version": "0.0.1436416", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1436416.tgz", + "integrity": "sha512-iGLhz2WOrlBLcTcoVsFy5dPPUqILG6cc8MITYd5lV6i38gWG14bMXRH/d8G5KITrWHBnbsOnWHfc9Qs4/jej9Q==", + "dev": true }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -648,47 +181,6 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -724,7 +216,6 @@ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -787,6 +278,18 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/lru-cache": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", @@ -796,27 +299,6 @@ "node": "14 || >=16.14" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -871,7 +353,6 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -932,7 +413,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -942,7 +422,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -1049,7 +528,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.14.0.tgz", "integrity": "sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "~0.18.20", "get-tsconfig": "^4.7.2", @@ -1070,7 +548,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -1087,7 +564,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -1104,7 +580,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -1121,7 +596,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1138,7 +612,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1155,7 +628,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1172,7 +644,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1189,7 +660,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1206,7 +676,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1223,7 +692,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1240,7 +708,6 @@ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1257,7 +724,6 @@ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1274,7 +740,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1291,7 +756,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1308,7 +772,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1325,7 +788,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1342,7 +804,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1359,7 +820,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1376,7 +836,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" @@ -1393,7 +852,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1410,7 +868,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1427,7 +884,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1442,7 +898,6 @@ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1475,9 +930,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1487,6 +942,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c5e010e..145b03d 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,34 @@ { "name": "@vscode/dwarf-debugging", - "version": "0.0.2", + "version": "0.0.3", "description": "Enables DWARF debugging of webassembly in js-debug", "main": "dist/index.js", "scripts": { - "test": "npm run build-meta && tsx test/mandelbrot.test.ts", - "build": "npm run build-native-ext-image && npm run copy-out-native-ext && npm run build-meta", - "build-meta": "rimraf dist && node esbuild.js && tsc src/index.ts --target es2021 --declaration --emitDeclarationOnly --outDir dist", - "build-native-ext-image": "docker build -t cxx-debug-extension ./chrome-cxx", - "copy-out-native-ext": "docker run -v ./chrome-cxx/mnt:/mnt cxx-debug-extension /bin/sh /mnt/do-copy.sh", + "test": "npm run build-meta && node --import tsx --test test/*.test.ts test/e2e/*.test.ts", + "build": "npm run build-native-ext-image && npm run build-native-ext && npm run build-meta", + "build-meta": "rimraf dist && tsc -p . && cp ./wasm/symbols-backend.build/stage-2/gen/* dist && cp src/*.d.ts dist && npm run vscode-js-debug-compat-dist", + "build-native-ext-image": "docker build --platform=linux/amd64 -t vscode-dwarf-debugging-wasm-builder ./wasm/builder", + "build-native-ext": "docker run --rm --platform=linux/amd64 -v ./wasm:/wasm vscode-dwarf-debugging-wasm-builder", + "vscode-js-debug-compat-dist": "mkdir -p chrome-cxx/mnt && cp dist/ExtensionAPI.d.ts chrome-cxx/mnt/extension-api.d.ts", "prepack": "npm run build-meta" }, "author": "Connor Peet ", + "contributors": [ + "Mikael Waltersson " + ], "license": "MIT", "devDependencies": { - "@chialab/esbuild-plugin-meta-url": "^0.17.7", - "@types/emscripten": "^1.39.7", + "@types/emscripten": "1.39.4", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.6.0", - "esbuild": "^0.25.0", + "@types/ws": "^8.14.1", + "devtools-protocol": "^0.0.1436416", + "js-yaml": "^4.1.0", "rimraf": "^5.0.1", "tsx": "^3.12.10", - "typescript": "^5.2.2" + "typescript": "^5.8.2" }, "dependencies": { "ws": "^8.14.1" } -} +} \ No newline at end of file diff --git a/pipeline.yml b/pipeline.yml index cd4e49a..d42a919 100644 --- a/pipeline.yml +++ b/pipeline.yml @@ -28,12 +28,9 @@ extends: - script: npm ci displayName: Install dependencies - - script: npm run build-native-ext-image + - script: npm run build displayName: Build CXX extension - - script: npm run copy-out-native-ext - displayName: Copy out extension - - script: npm run test displayName: Test diff --git a/src/CustomFormatters.ts b/src/CustomFormatters.ts index e335af2..ec4e7dc 100644 --- a/src/CustomFormatters.ts +++ b/src/CustomFormatters.ts @@ -3,14 +3,14 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/CustomFormatters.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import type { Chrome } from './ExtensionAPI'; -import type {WasmValue} from './WasmTypes.js'; -import type {HostInterface} from './WorkerRPC.js'; +import type { WasmValue } from './WasmTypes'; +import type { HostInterface } from './WorkerRPC'; export interface FieldInfo { typeId: string; - name: string|undefined; + name: string | undefined; offset: number; } @@ -55,7 +55,7 @@ export interface Value { asFloat32: () => number; asFloat64: () => number; asDataView: (offset?: number, size?: number) => DataView; - $: (member: string|number) => Value; + $: (member: string | number) => Value; getMembers(): string[]; } @@ -123,11 +123,11 @@ export class PageStore { return begin - 1; } - findSlice(offset: number): MemorySlice|null { + findSlice(offset: number): MemorySlice | null { return this.getSlice(this.findSliceIndex(offset), offset); } - private getSlice(index: number, offset: number): MemorySlice|null { + private getSlice(index: number, offset: number): MemorySlice | null { if (index < 0) { return null; } @@ -135,7 +135,7 @@ export class PageStore { return candidate?.contains(offset) ? candidate : null; } - addSlice(buffer: ArrayBuffer|number[], begin: number): MemorySlice { + addSlice(buffer: ArrayBuffer | number[], begin: number): MemorySlice { let slice = new MemorySlice(Array.isArray(buffer) ? new Uint8Array(buffer).buffer : buffer, begin); let leftPosition = this.findSliceIndex(slice.begin - 1); @@ -151,12 +151,13 @@ export class PageStore { slice = slice.merge(rightOverlap); } this.slices.splice( - leftPosition, // Insert to the right if no overlap - rightPosition - leftPosition + 1, // Delete one additional slice if overlapping on the left - slice); + leftPosition, // Insert to the right if no overlap + rightPosition - leftPosition + 1, // Delete one additional slice if overlapping on the left + slice); return slice; } } + export class WasmMemoryView { private readonly wasm: WasmInterface; private readonly pages = new PageStore(); @@ -166,13 +167,13 @@ export class WasmMemoryView { this.wasm = wasm; } - private page(byteOffset: number, byteLength: number): {page: number, offset: number, count: number} { + private page(byteOffset: number, byteLength: number): { page: number, offset: number, count: number; } { const mask = WasmMemoryView.PAGE_SIZE - 1; const offset = byteOffset & mask; const page = byteOffset - offset; const rangeEnd = byteOffset + byteLength; const count = 1 + Math.ceil((rangeEnd - (rangeEnd & mask) - page) / WasmMemoryView.PAGE_SIZE); - return {page, offset, count}; + return { page, offset, count }; } private getPages(page: number, count: number): DataView { @@ -192,57 +193,57 @@ export class WasmMemoryView { } getFloat32(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 4); + const { offset, page, count } = this.page(byteOffset, 4); const view = this.getPages(page, count); return view.getFloat32(offset, littleEndian); } getFloat64(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 8); + const { offset, page, count } = this.page(byteOffset, 8); const view = this.getPages(page, count); return view.getFloat64(offset, littleEndian); } getInt8(byteOffset: number): number { - const {offset, page, count} = this.page(byteOffset, 1); + const { offset, page, count } = this.page(byteOffset, 1); const view = this.getPages(page, count); return view.getInt8(offset); } getInt16(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 2); + const { offset, page, count } = this.page(byteOffset, 2); const view = this.getPages(page, count); return view.getInt16(offset, littleEndian); } getInt32(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 4); + const { offset, page, count } = this.page(byteOffset, 4); const view = this.getPages(page, count); return view.getInt32(offset, littleEndian); } getUint8(byteOffset: number): number { - const {offset, page, count} = this.page(byteOffset, 1); + const { offset, page, count } = this.page(byteOffset, 1); const view = this.getPages(page, count); return view.getUint8(offset); } getUint16(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 2); + const { offset, page, count } = this.page(byteOffset, 2); const view = this.getPages(page, count); return view.getUint16(offset, littleEndian); } getUint32(byteOffset: number, littleEndian?: boolean): number { - const {offset, page, count} = this.page(byteOffset, 4); + const { offset, page, count } = this.page(byteOffset, 4); const view = this.getPages(page, count); return view.getUint32(offset, littleEndian); } getBigInt64(byteOffset: number, littleEndian?: boolean): bigint { - const {offset, page, count} = this.page(byteOffset, 8); + const { offset, page, count } = this.page(byteOffset, 8); const view = this.getPages(page, count); return view.getBigInt64(offset, littleEndian); } getBigUint64(byteOffset: number, littleEndian?: boolean): bigint { - const {offset, page, count} = this.page(byteOffset, 8); + const { offset, page, count } = this.page(byteOffset, 8); const view = this.getPages(page, count); return view.getBigUint64(offset, littleEndian); } asDataView(byteOffset: number, byteLength: number): DataView { - const {offset, page, count} = this.page(byteOffset, byteLength); + const { offset, page, count } = this.page(byteOffset, byteLength); const view = this.getPages(page, count); return new DataView(view.buffer, view.byteOffset + offset, byteLength); } @@ -252,19 +253,19 @@ export class CXXValue implements Value, LazyObject { readonly location: number; private readonly type: TypeInfo; private readonly data?: number[]; - private readonly memoryOrDataView: DataView|WasmMemoryView; + private readonly memoryOrDataView: DataView | WasmMemoryView; private readonly wasm: WasmInterface; private readonly typeMap: Map; private readonly memoryView: WasmMemoryView; - private membersMap?: Map; + private membersMap?: Map; private readonly objectStore: LazyObjectStore; private readonly objectId: string; - private readonly displayValue: string|undefined; + private readonly displayValue: string | undefined; private readonly memoryAddress?: number; constructor( - objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, location: number, type: TypeInfo, - typeMap: Map, data?: number[], displayValue?: string, memoryAddress?: number) { + objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, location: number, type: TypeInfo, + typeMap: Map, data?: number[], displayValue?: string, memoryAddress?: number) { if (!location && !data) { throw new Error('Cannot represent nullptr'); } @@ -296,19 +297,19 @@ export class CXXValue implements Value, LazyObject { for (const info of typeInfo.typeInfos) { typeMap.set(info.typeId, info); } - const {location, root, data, displayValue, memoryAddress} = typeInfo; + const { location, root, data, displayValue, memoryAddress } = typeInfo; return new CXXValue(objectStore, wasm, memoryView, location ?? 0, root, typeMap, data, displayValue, memoryAddress); } - private get members(): Map { + private get members(): Map { if (!this.membersMap) { this.membersMap = new Map(); for (const member of this.type.members) { const memberType = this.typeMap.get(member.typeId); if (memberType && member.name) { const memberLocation = member.name === '*' ? this.memoryOrDataView.getUint32(this.location, true) : - this.location + member.offset; - this.membersMap.set(member.name, {location: memberLocation, type: memberType}); + this.location + member.offset; + this.membersMap.set(member.name, { location: memberLocation, type: memberType }); } } } @@ -322,56 +323,56 @@ export class CXXValue implements Value, LazyObject { throw new Error(`Incomplete type information for array or pointer type '${this.typeNames}'`); } return new CXXValue( - this.objectStore, this.wasm, this.memoryView, element.location + index * element.type.size, element.type, - this.typeMap, data); + this.objectStore, this.wasm, this.memoryView, element.location + index * element.type.size, element.type, + this.typeMap, data); } - async getProperties(): Promise> { + async getProperties(): Promise> { const properties = []; if (this.type.arraySize > 0) { for (let index = 0; index < this.type.arraySize; ++index) { - properties.push({name: `${index}`, property: await this.getArrayElement(index)}); + properties.push({ name: `${index}`, property: await this.getArrayElement(index) }); } } else { const members = await this.members; const data = members.has('*') ? undefined : this.data; - for (const [name, {location, type}] of members) { + for (const [name, { location, type }] of members) { const property = new CXXValue(this.objectStore, this.wasm, this.memoryView, location, type, this.typeMap, data); - properties.push({name, property}); + properties.push({ name, property }); } } return properties; } - async asRemoteObject(): Promise { + async asRemoteObject(): Promise { if (this.type.hasValue && this.type.arraySize === 0) { const formatter = CustomFormatters.get(this.type); if (!formatter) { const type = 'undefined' as Chrome.DevTools.RemoteObjectType; const description = ''; - return {type, description, hasChildren: false}; + return { type, description, hasChildren: false }; } if (this.location === undefined || (!this.data && this.location === 0xffffffff)) { const type = 'undefined' as Chrome.DevTools.RemoteObjectType; const description = ''; - return {type, description, hasChildren: false}; + return { type, description, hasChildren: false }; } const value = - new CXXValue(this.objectStore, this.wasm, this.memoryView, this.location, this.type, this.typeMap, this.data); + new CXXValue(this.objectStore, this.wasm, this.memoryView, this.location, this.type, this.typeMap, this.data); try { const formattedValue = await formatter.format(this.wasm, value); return await lazyObjectFromAny( - formattedValue, this.objectStore, this.type, this.displayValue, this.memoryAddress) - .asRemoteObject(); + formattedValue, this.objectStore, this.type, this.displayValue, this.memoryAddress) + .asRemoteObject(); } catch { // Fallthrough } } const type = (this.type.arraySize > 0 ? 'array' : 'object') as Chrome.DevTools.RemoteObjectType; - const {objectId} = this; + const { objectId } = this; return { type, description: this.type.typeNames[0], @@ -429,11 +430,11 @@ export class CXXValue implements Value, LazyObject { throw new RangeError('Size exceeds the buffer range'); } return new DataView( - this.memoryOrDataView.buffer, this.memoryOrDataView.byteOffset + this.location + offset, size); + this.memoryOrDataView.buffer, this.memoryOrDataView.byteOffset + this.location + offset, size); } return this.memoryView.asDataView(offset, size); } - $(selector: string|number): CXXValue { + $(selector: string | number): CXXValue { const data = this.members.has('*') ? undefined : this.data; if (typeof selector === 'number') { @@ -446,11 +447,10 @@ export class CXXValue implements Value, LazyObject { const member = this.members.get(memberName); if (!member) { - throw new Error(`Type ${this.typeNames[0] || ''} has no member '${ - memberName}'. Available members are: ${Array.from(this.members.keys())}`); + throw new Error(`Type ${this.typeNames[0] || ''} has no member '${memberName}'. Available members are: ${Array.from(this.members.keys())}`); } const memberValue = - new CXXValue(this.objectStore, this.wasm, this.memoryView, member.location, member.type, this.typeMap, data); + new CXXValue(this.objectStore, this.wasm, this.memoryView, member.location, member.type, this.typeMap, data); if (selector.length === 0) { return memberValue; } @@ -463,12 +463,12 @@ export class CXXValue implements Value, LazyObject { } export interface LazyObject { - getProperties(): Promise>; - asRemoteObject(): Promise; + getProperties(): Promise>; + asRemoteObject(): Promise; } export function primitiveObject( - value: T, description?: string, linearMemoryAddress?: number, type?: TypeInfo): PrimitiveLazyObject|null { + value: T, description?: string, linearMemoryAddress?: number, type?: TypeInfo): PrimitiveLazyObject | null { if (['number', 'string', 'boolean', 'bigint', 'undefined'].includes(typeof value)) { if (typeof value === 'bigint' || typeof value === 'number') { const enumerator = type?.enumerators?.find(e => e.value === BigInt(value)); @@ -477,14 +477,14 @@ export function primitiveObject( } } return new PrimitiveLazyObject( - typeof value as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress, type?.size); + typeof value as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress, type?.size); } return null; } function lazyObjectFromAny( - value: FormatterResult, objectStore: LazyObjectStore, type?: TypeInfo, description?: string, - linearMemoryAddress?: number): LazyObject { + value: FormatterResult, objectStore: LazyObjectStore, type?: TypeInfo, description?: string, + linearMemoryAddress?: number): LazyObject { const primitive = primitiveObject(value, description, linearMemoryAddress, type); if (primitive) { return primitive; @@ -495,7 +495,7 @@ function lazyObjectFromAny( if (typeof value === 'object') { if (value === null) { return new PrimitiveLazyObject( - 'null' as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress); + 'null' as Chrome.DevTools.RemoteObjectType, value, description, linearMemoryAddress); } return new LocalLazyObject(value, objectStore, type, linearMemoryAddress); } @@ -516,7 +516,7 @@ export class LazyObjectStore { return objectId; } - get(objectId: string): LazyObject|undefined { + get(objectId: string): LazyObject | undefined { return this.objects.get(objectId); } @@ -536,8 +536,8 @@ export class PrimitiveLazyObject implements LazyObject { private readonly linearMemoryAddress?: number; private readonly linearMemorySize?: number; constructor( - type: Chrome.DevTools.RemoteObjectType, value: T, description?: string, linearMemoryAddress?: number, - linearMemorySize?: number) { + type: Chrome.DevTools.RemoteObjectType, value: T, description?: string, linearMemoryAddress?: number, + linearMemorySize?: number) { this.type = type; this.value = value; this.description = description ?? `${value}`; @@ -545,13 +545,13 @@ export class PrimitiveLazyObject implements LazyObject { this.linearMemorySize = linearMemorySize; } - async getProperties(): Promise> { + async getProperties(): Promise> { return []; } async asRemoteObject(): Promise { - const {type, value, description, linearMemoryAddress, linearMemorySize} = this; - return {type, hasChildren: false, value, description, linearMemoryAddress, linearMemorySize}; + const { type, value, description, linearMemoryAddress, linearMemorySize } = this; + return { type, hasChildren: false, value, description, linearMemoryAddress, linearMemorySize }; } } @@ -570,16 +570,16 @@ export class LocalLazyObject implements LazyObject { this.linearMemoryAddress = linearMemoryAddress; } - async getProperties(): Promise> { + async getProperties(): Promise> { return Object.entries(this.value).map(([name, value]) => { const property = lazyObjectFromAny(value, this.objectStore); - return {name, property}; + return { name, property }; }); } async asRemoteObject(): Promise { const type = (Array.isArray(this.value) ? 'array' : 'object') as Chrome.DevTools.RemoteObjectType; - const {objectId, type: valueType, linearMemoryAddress} = this; + const { objectId, type: valueType, linearMemoryAddress } = this; return { type, objectId, @@ -591,10 +591,11 @@ export class LocalLazyObject implements LazyObject { } } -export type FormatterResult = number|string|boolean|bigint|undefined|CXXValue|object|(() => LazyObject); +export type FormatterResult = number | string | boolean | bigint | undefined | CXXValue | object | (() => LazyObject); export type FormatterCallback = (wasm: WasmInterface, value: Value) => FormatterResult; + export interface Formatter { - types: string[]|((t: TypeInfo) => boolean); + types: string[] | ((t: TypeInfo) => boolean); imports?: FormatterCallback[]; format: FormatterCallback; } @@ -661,7 +662,7 @@ export class CustomFormatters { } } - static get(type: TypeInfo): Formatter|null { + static get(type: TypeInfo): Formatter | null { for (const name of type.typeNames) { const formatter = CustomFormatters.formatters.get(name); if (formatter) { diff --git a/src/DWARFLanguageExtensionPlugin.ts b/src/DWARFLanguageExtensionPlugin.ts index 6546fb8..8754bb4 100644 --- a/src/DWARFLanguageExtensionPlugin.ts +++ b/src/DWARFLanguageExtensionPlugin.ts @@ -3,20 +3,15 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/DWARFSymbols.ts -import './Formatters.js'; +import './Formatters'; -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import type { Chrome } from './ExtensionAPI'; -import * as Formatters from './CustomFormatters.js'; -import { - DEFAULT_MODULE_CONFIGURATIONS, - findModuleConfiguration, - type ModuleConfigurations, - resolveSourcePathToURL, -} from './ModuleConfiguration.js'; -import type * as SymbolsBackend from './SymbolsBackend.js'; -import createSymbolsBackend from './SymbolsBackend.js'; -import type {HostInterface} from './WorkerRPC.js'; +import * as Formatters from './CustomFormatters'; +import { resolveSourcePathToURL } from './PathUtils'; +import type * as SymbolsBackend from './SymbolsBackend'; +import createSymbolsBackend from './SymbolsBackend'; +import type { HostInterface } from './WorkerRPC'; function mapVector(vector: SymbolsBackend.Vector, callback: (apiElement: ApiT) => T): T[] { const elements: T[] = []; @@ -28,19 +23,19 @@ function mapVector(vector: SymbolsBackend.Vector, callback: (apiE } interface ScopeInfo { - type: 'GLOBAL'|'LOCAL'|'PARAMETER'; + type: 'GLOBAL' | 'LOCAL' | 'PARAMETER'; typeName: string; icon?: string; } -type LazyFSNode = FS.FSNode&{contents: {cacheLength: () => void, length: number}}; +type LazyFSNode = FS.FSNode & { contents: { cacheLength: () => void, length: number; }; }; function mapEnumerator(apiEnumerator: SymbolsBackend.Enumerator): Formatters.Enumerator { - return {typeId: apiEnumerator.typeId, value: apiEnumerator.value, name: apiEnumerator.name}; + return { typeId: apiEnumerator.typeId, value: apiEnumerator.value, name: apiEnumerator.name }; } function mapFieldInfo(apiFieldInfo: SymbolsBackend.FieldInfo): Formatters.FieldInfo { - return {typeId: apiFieldInfo.typeId, offset: apiFieldInfo.offset, name: apiFieldInfo.name}; + return { typeId: apiFieldInfo.typeId, offset: apiFieldInfo.offset, name: apiFieldInfo.name }; } class ModuleInfo { @@ -49,14 +44,14 @@ class ModuleInfo { readonly dwarfSymbolsPlugin: SymbolsBackend.DWARFSymbolsPlugin; constructor( - readonly symbolsUrl: string, readonly symbolsFileName: string, readonly symbolsDwpFileName: string|undefined, - readonly backend: SymbolsBackend.Module) { + readonly symbolsUrl: string, readonly symbolsFileName: string, readonly symbolsDwpFileName: string | undefined, + readonly backend: SymbolsBackend.Module) { this.fileNameToUrl = new Map(); this.urlToFileName = new Map(); this.dwarfSymbolsPlugin = new backend.DWARFSymbolsPlugin(); } - stringifyScope(scope: SymbolsBackend.VariableScope): 'GLOBAL'|'LOCAL'|'PARAMETER' { + stringifyScope(scope: SymbolsBackend.VariableScope): 'GLOBAL' | 'LOCAL' | 'PARAMETER' { switch (scope) { case this.backend.VariableScope.GLOBAL: return 'GLOBAL'; @@ -83,9 +78,9 @@ class ModuleInfo { } } -export function createEmbindPool(): { +function createEmbindPool(): { flush(): void, - manage(object: T): T, + manage(object: T): T, unmanage(object: T): boolean, } { class EmbindObjectPool { @@ -98,7 +93,7 @@ export function createEmbindPool(): { this.objectPool = []; } - manage(object: T): T { + manage(object: T): T { if (typeof object !== 'undefined') { this.objectPool.push(object); } @@ -120,79 +115,79 @@ export function createEmbindPool(): { const manage = pool.manage.bind(pool); const unmanage = pool.unmanage.bind(pool); const flush = pool.flush.bind(pool); - return {manage, unmanage, flush}; + return { manage, unmanage, flush }; } // Cache the underlying WebAssembly module after the first instantiation // so that subsequent calls to `createSymbolsBackend()` are faster, which // greatly speeds up the test suite. -let symbolsBackendModulePromise: undefined|Promise; +let symbolsBackendModulePromise: undefined | Promise; function instantiateWasm( - imports: WebAssembly.Imports, - callback: (module: WebAssembly.Module) => void, - resourceLoader: ResourceLoader, - ): Emscripten.WebAssemblyExports { + imports: WebAssembly.Imports, + callback: (module: WebAssembly.Module) => void, + resourceLoader: ResourceLoader, +): Emscripten.WebAssemblyExports { if (!symbolsBackendModulePromise) { symbolsBackendModulePromise = resourceLoader.createSymbolsBackendModulePromise(); } symbolsBackendModulePromise.then(module => WebAssembly.instantiate(module, imports)) - .then(callback) - .catch(console.error); + .then(callback) + .catch(console.error); return []; } -export type RawModule = Chrome.DevTools.RawModule&{dwp?: ArrayBuffer}; +export type RawModule = Chrome.DevTools.RawModule & { dwp?: ArrayBuffer; }; export interface ResourceLoader { loadSymbols(rawModuleId: string, rawModule: RawModule, url: URL, filesystem: typeof FS, hostInterface: HostInterface): - Promise<{symbolsFileName: string, symbolsDwpFileName?: string}>; + Promise<{ symbolsFileName: string, symbolsDwpFileName?: string; }>; createSymbolsBackendModulePromise(): Promise; possiblyMissingSymbols?: string[]; } export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExtensionPlugin { - private moduleInfos = new Map>(); + private moduleInfos = new Map>(); private lazyObjects = new Formatters.LazyObjectStore(); - constructor( - readonly moduleConfigurations: ModuleConfigurations, readonly resourceLoader: ResourceLoader, - readonly hostInterface: HostInterface) { - this.moduleConfigurations = moduleConfigurations; - } + constructor(readonly resourceLoader: ResourceLoader, readonly hostInterface: HostInterface) { } private async newModuleInfo(rawModuleId: string, symbolsHint: string, rawModule: RawModule): Promise { - const {flush, manage} = createEmbindPool(); + const { flush, manage } = createEmbindPool(); try { const rawModuleURL = new URL(rawModule.url); - const {pathSubstitutions} = findModuleConfiguration(this.moduleConfigurations, rawModuleURL); - const symbolsURL = symbolsHint ? resolveSourcePathToURL([], symbolsHint, rawModuleURL) : rawModuleURL; + const symbolsURL = symbolsHint ? resolveSourcePathToURL(symbolsHint, rawModuleURL) : rawModuleURL; const instantiateWasmWrapper = - (imports: Emscripten.WebAssemblyImports, - callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { - // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. - return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); - }; - const backend = await createSymbolsBackend({instantiateWasm: instantiateWasmWrapper}); - const {symbolsFileName, symbolsDwpFileName} = - await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); + (imports: Emscripten.WebAssemblyImports, + callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { + // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. + return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); + }; + const backend = await createSymbolsBackend({ instantiateWasm: instantiateWasmWrapper }); + const { symbolsFileName, symbolsDwpFileName } = + await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); const moduleInfo = new ModuleInfo(symbolsURL.href, symbolsFileName, symbolsDwpFileName, backend); const addRawModuleResponse = manage(moduleInfo.dwarfSymbolsPlugin.AddRawModule(rawModuleId, symbolsFileName)); mapVector(manage(addRawModuleResponse.sources), fileName => { - const fileURL = resolveSourcePathToURL(pathSubstitutions, fileName, symbolsURL); + const fileURL = resolveSourcePathToURL(fileName, symbolsURL); moduleInfo.fileNameToUrl.set(fileName, fileURL.href); moduleInfo.urlToFileName.set(fileURL.href, fileName); }); - // Set up lazy dwo files if we are running on a worker - if (typeof global === 'undefined' && typeof importScripts === 'function' && - typeof XMLHttpRequest !== 'undefined') { - mapVector(manage(addRawModuleResponse.dwos), dwoFile => { - const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; - const pathSplit = absolutePath.split('/'); - const fileName = pathSplit.pop() as string; - const parentDirectory = pathSplit.join('/'); + for (const dwoFile of mapVector(manage(addRawModuleResponse.dwos), dwo => dwo)) { + const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; + const pathSplit = absolutePath.split('/'); + const fileName = pathSplit.pop() as string; + const parentDirectory = pathSplit.join('/'); + + const dwoURL = new URL(dwoFile, symbolsURL).href; + const dwoResponse = await fetch(dwoURL, { mode: 'no-cors' }) + .catch((e: Error) => ({ ok: false, status: -1, statusText: e.message } as const)); + + if (dwoResponse.ok) { + const dwoData = await dwoResponse.arrayBuffer(); + void this.hostInterface.reportResourceLoad(dwoURL, { success: true, size: dwoData.byteLength }); // Sometimes these stick around. try { @@ -206,23 +201,18 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt backend.FS.createPath('/', parentDirectory.substring(1), true, true); } - const dwoURL = new URL(dwoFile, symbolsURL).href; - const node = backend.FS.createLazyFile(parentDirectory, fileName, dwoURL, true, false) as LazyFSNode; - const cacheLength = node.contents.cacheLength; - const wrapper = (): void => { - try { - cacheLength.apply(node.contents); - void this.hostInterface.reportResourceLoad(dwoURL, {success: true, size: node.contents.length}); - } catch (e) { - void this.hostInterface.reportResourceLoad(dwoURL, {success: false, errorMessage: (e as Error).message}); - // Rethrow any error fetching the content as errno 44 (EEXIST) - // TypeScript doesn't know about the ErrnoError constructor - // @ts-expect-error doesn't exit on types - throw new backend.FS.ErrnoError(44); - } - }; - node.contents.cacheLength = wrapper; - }); + backend.FS.createDataFile( + parentDirectory, + fileName, + new Uint8Array(dwoData), + true /* canRead */, + false /* canWrite */, + true /* canOwn */, + ); + } else { + const dwoError = dwoResponse.statusText || `status code ${dwoResponse.status}`; + void this.hostInterface.reportResourceLoad(dwoURL, { success: false, errorMessage: `Failed to fetch dwo file: ${dwoError}` }); + } } return moduleInfo; @@ -275,8 +265,8 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): - Promise { - const {flush, manage} = createEmbindPool(); + Promise { + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(sourceLocation.rawModuleId); const sourceFile = moduleInfo.urlToFileName.get(sourceLocation.sourceFileURL); if (!sourceFile) { @@ -284,14 +274,14 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } try { const rawLocations = manage(moduleInfo.dwarfSymbolsPlugin.SourceLocationToRawLocation( - sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber)); + sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const {rawModuleId, startOffset, endOffset} = manage(rawLocation); - return {rawModuleId, startOffset, endOffset}; + const { rawModuleId, startOffset, endOffset } = manage(rawLocation); + return { rawModuleId, startOffset, endOffset }; }); return locations; } finally { @@ -300,12 +290,12 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): - Promise { - const {flush, manage} = createEmbindPool(); + Promise { + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const sourceLocations = moduleInfo.dwarfSymbolsPlugin.RawLocationToSourceLocation( - rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); const error = manage(sourceLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); @@ -315,7 +305,7 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt if (!sourceFileURL) { throw new Error(`InternalError: Unknown source file ${sourceLocation.sourceFile}`); } - const {rawModuleId, lineNumber, columnNumber} = manage(sourceLocation); + const { rawModuleId, lineNumber, columnNumber } = manage(sourceLocation); return { rawModuleId, sourceFileURL, @@ -354,18 +344,18 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async listVariablesInScope(rawLocation: Chrome.DevTools.RawLocation): Promise { - const {flush, manage} = createEmbindPool(); + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const variables = manage(moduleInfo.dwarfSymbolsPlugin.ListVariablesInScope( - rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0)); + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0)); const error = manage(variables.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const apiVariables = mapVector(manage(variables.variable), variable => { - const {scope, name, type} = manage(variable); - return {scope: moduleInfo.stringifyScope(scope), name, type, nestedName: name.split('::')}; + const { scope, name, type } = manage(variable); + return { scope: moduleInfo.stringifyScope(scope), name, type, nestedName: name.split('::') }; }); return apiVariables; } finally { @@ -374,18 +364,18 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): - Promise<{frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]}> { - const {flush, manage} = createEmbindPool(); + Promise<{ frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]; }> { + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const functionInfo = - manage(moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset)); + manage(moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(functionInfo.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const apiFunctionInfos = mapVector(manage(functionInfo.functionNames), functionName => { - return {name: functionName}; + return { name: functionName }; }); let apiMissingSymbolFiles = mapVector(manage(functionInfo.missingSymbolFiles), x => x); if (apiMissingSymbolFiles.length && this.resourceLoader.possiblyMissingSymbols) { @@ -402,19 +392,19 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): - Promise { - const {flush, manage} = createEmbindPool(); + Promise { + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const rawLocations = manage( - moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); + moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const {rawModuleId, startOffset, endOffset} = manage(rawLocation); - return {rawModuleId, startOffset, endOffset}; + const { rawModuleId, startOffset, endOffset } = manage(rawLocation); + return { rawModuleId, startOffset, endOffset }; }); return locations; } finally { @@ -423,18 +413,18 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { - const {flush, manage} = createEmbindPool(); + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const rawLocations = manage( - moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); + moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const {rawModuleId, startOffset, endOffset} = manage(rawLocation); - return {rawModuleId, startOffset, endOffset}; + const { rawModuleId, startOffset, endOffset } = manage(rawLocation); + return { rawModuleId, startOffset, endOffset }; }); return locations; } finally { @@ -449,8 +439,8 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt data?: number[], displayValue?: string, memoryAddress?: number, - }|null> { - const {manage, unmanage, flush} = createEmbindPool(); + } | null> { + const { manage, unmanage, flush } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(context.rawModuleId); try { const apiRawLocation = manage(new moduleInfo.backend.RawLocation()); @@ -461,7 +451,7 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); const proxy = new Formatters.DebuggerProxy(wasm, moduleInfo.backend); const typeInfoResult = - manage(moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, proxy)); + manage(moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, proxy)); const error = manage(typeInfoResult.error); if (error) { if (error.code === moduleInfo.backend.ErrorCode.MODULE_NOT_FOUND_ERROR) { @@ -478,9 +468,9 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt const typeInfos = mapVector(manage(typeInfoResult.typeInfos), typeInfo => fromApiTypeInfo(manage(typeInfo))); const root = fromApiTypeInfo(manage(typeInfoResult.root)); - const {location, displayValue, memoryAddress} = typeInfoResult; + const { location, displayValue, memoryAddress } = typeInfoResult; const data = typeInfoResult.data ? mapVector(manage(typeInfoResult.data), n => n) : undefined; - return {typeInfos, root, location, data, displayValue, memoryAddress}; + return { typeInfos, root, location, data, displayValue, memoryAddress }; function fromApiTypeInfo(apiTypeInfo: SymbolsBackend.TypeInfo): Formatters.TypeInfo { const apiMembers = manage(apiTypeInfo.members); @@ -490,7 +480,7 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt unmanage(apiEnumerators); const typeNames = mapVector(manage(apiTypeInfo.typeNames), e => e); unmanage(apiMembers); - const {typeId, size, arraySize, alignment, canExpand, isPointer, hasValue} = apiTypeInfo; + const { typeId, size, arraySize, alignment, canExpand, isPointer, hasValue } = apiTypeInfo; const formatter = Formatters.CustomFormatters.get({ typeNames, typeId, @@ -522,7 +512,7 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { - const {flush, manage} = createEmbindPool(); + const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawModuleId); const sourceFile = moduleInfo.urlToFileName.get(sourceFileURL); if (!sourceFile) { @@ -543,7 +533,7 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async evaluate(expression: string, context: SymbolsBackend.RawLocation, stopId: unknown): - Promise { + Promise { const valueInfo = await this.getValueInfo(expression, context, stopId); if (!valueInfo) { return null; @@ -569,8 +559,8 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt const properties = await remoteObject.getProperties(); const descriptors = []; - for (const {name, property} of properties) { - descriptors.push({name, value: await property.asRemoteObject()}); + for (const { name, property } of properties) { + descriptors.push({ name, value: await property.asRemoteObject() }); } return descriptors; } @@ -581,33 +571,32 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } export async function createPlugin( - hostInterface: HostInterface, resourceLoader: ResourceLoader, - moduleConfigurations: ModuleConfigurations = DEFAULT_MODULE_CONFIGURATIONS, - logPluginApiCalls = false): Promise { - const plugin = new DWARFLanguageExtensionPlugin(moduleConfigurations, resourceLoader, hostInterface); + hostInterface: HostInterface, resourceLoader: ResourceLoader, + logPluginApiCalls = false): Promise { + const plugin = new DWARFLanguageExtensionPlugin(resourceLoader, hostInterface); if (logPluginApiCalls) { const pluginLoggingProxy = { - get: function(target: DWARFLanguageExtensionPlugin, key: Key): - DWARFLanguageExtensionPlugin[Key] { - if (typeof target[key] === 'function') { - return function(): unknown { - const args = [...arguments]; - const jsonArgs = args.map(x => { - try { - return JSON.stringify(x); - } catch { - return x.toString(); - } - }) - .join(', '); - // eslint-disable-next-line no-console - console.info(`${key}(${jsonArgs})`); - // @ts-expect-error TypeScript does not play well with `arguments` - return (target[key] as (...args: any[]) => void).apply(target, arguments); - } as unknown as DWARFLanguageExtensionPlugin[Key]; - } - return Reflect.get(target, key); - }, + get: function (target: DWARFLanguageExtensionPlugin, key: Key): + DWARFLanguageExtensionPlugin[Key] { + if (typeof target[key] === 'function') { + return function (): unknown { + const args = [...arguments]; + const jsonArgs = args.map(x => { + try { + return JSON.stringify(x); + } catch { + return x.toString(); + } + }) + .join(', '); + // eslint-disable-next-line no-console + console.info(`${key}(${jsonArgs})`); + // @ts-expect-error TypeScript does not play well with `arguments` + return (target[key] as (...args: any[]) => void).apply(target, arguments); + } as unknown as DWARFLanguageExtensionPlugin[Key]; + } + return Reflect.get(target, key); + }, }; return new Proxy(plugin, pluginLoggingProxy); diff --git a/src/ExtensionAPI.d.ts b/src/ExtensionAPI.d.ts index 1ac4212..856a520 100644 --- a/src/ExtensionAPI.d.ts +++ b/src/ExtensionAPI.d.ts @@ -5,126 +5,6 @@ export namespace Chrome { export namespace DevTools { - export interface EventSink void> { - addListener(listener: ListenerT): void; - removeListener(listener: ListenerT): void; - } - - export interface Resource { - readonly url: string; - readonly type: string; - - getContent(callback: (content: string, encoding: string) => unknown): void; - setContent(content: string, commit: boolean, callback?: (error?: Object) => unknown): void; - /** - * Augments this resource's scopes information based on the list of {@link NamedFunctionRange}s - * for improved debuggability and function naming. - * - * @throws - * If this resource was not produced by a sourcemap or if {@link ranges} are not nested properly. - * Concretely: For each range, start position must be less than end position, and - * there must be no "straddling" (i.e. partially overlapping ranges). - */ - setFunctionRangesForScript(ranges: NamedFunctionRange[]): Promise; - attachSourceMapURL(sourceMapURL: string): Promise; - } - - export interface InspectedWindow { - tabId: number; - - onResourceAdded: EventSink<(resource: Resource) => unknown>; - onResourceContentCommitted: EventSink<(resource: Resource, content: string) => unknown>; - - eval( - expression: string, - options?: {scriptExecutionContext?: string, frameURL?: string, useContentScriptContext?: boolean}, - callback?: (result: unknown, exceptioninfo: { - code: string, - description: string, - details: unknown[], - isError: boolean, - isException: boolean, - value: string - }) => unknown): void; - getResources(callback: (resources: Resource[]) => unknown): void; - reload(reloadOptions?: {ignoreCache?: boolean, injectedScript?: string, userAgent?: string}): void; - } - - export interface Button { - onClicked: EventSink<() => unknown>; - update(iconPath?: string, tooltipText?: string, disabled?: boolean): void; - } - - export interface ExtensionView { - onHidden: EventSink<() => unknown>; - onShown: EventSink<(window?: Window) => unknown>; - } - - export interface ExtensionPanel extends ExtensionView { - show(): void; - onSearch: EventSink<(action: string, queryString?: string) => unknown>; - createStatusBarButton(iconPath: string, tooltipText: string, disabled: boolean): Button; - } - - export interface RecorderView extends ExtensionView { - show(): void; - } - - export interface ExtensionSidebarPane extends ExtensionView { - setHeight(height: string): void; - setObject(jsonObject: string, rootTitle?: string, callback?: () => unknown): void; - setPage(path: string): void; - } - - export interface PanelWithSidebar { - createSidebarPane(title: string, callback?: (result: ExtensionSidebarPane) => unknown): void; - onSelectionChanged: EventSink<() => unknown>; - } - - export interface Panels { - elements: PanelWithSidebar; - sources: PanelWithSidebar; - network: NetworkPanel; - themeName: string; - - create(title: string, iconPath: string, pagePath: string, callback?: (panel: ExtensionPanel) => unknown): void; - openResource(url: string, lineNumber: number, columnNumber?: number, callback?: () => unknown): void; - - /** - * Fired when the theme changes in DevTools. - * - * @param callback The handler callback to register and be invoked on theme changes. - */ - setThemeChangeHandler(callback?: (themeName: string) => unknown): void; - } - - export interface Request { - getContent(callback: (content: string, encoding: string) => unknown): void; - } - - export interface Network { - onNavigated: EventSink<(url: string) => unknown>; - onRequestFinished: EventSink<(request: Request) => unknown>; - - getHAR(callback: (harLog: object) => unknown): void; - } - - export interface NetworkPanel { - show(options?: {filter: string}): Promise; - } - - export interface DevToolsAPI { - network: Network; - panels: Panels; - inspectedWindow: InspectedWindow; - languageServices: LanguageExtensions; - recorder: RecorderExtensions; - performance: Performance; - } - - export interface ExperimentalDevToolsAPI { - inspectedWindow: InspectedWindow; - } export interface RawModule { url: string; @@ -167,18 +47,8 @@ export namespace Chrome { name: string; } - export type RecorderExtensionPlugin = RecorderExtensionExportPlugin|RecorderExtensionReplayPlugin; - - export interface RecorderExtensionExportPlugin { - stringify(recording: Record): Promise; - stringifyStep(step: Record): Promise; - } - export interface RecorderExtensionReplayPlugin { - replay(recording: Record): void; - } - export type RemoteObjectId = string; - export type RemoteObjectType = 'object'|'undefined'|'string'|'number'|'boolean'|'bigint'|'array'|'null'; + export type RemoteObjectType = 'object' | 'undefined' | 'string' | 'number' | 'boolean' | 'bigint' | 'array' | 'null'; export interface RemoteObject { type: RemoteObjectType; @@ -199,13 +69,29 @@ export namespace Chrome { */ export interface ForeignObject { type: 'reftype'; - valueClass: 'local'|'global'|'operand'; + valueClass: 'local' | 'global' | 'operand'; index: number; + /** + * @deprecated Only for type compatibility with @vscode/js-debug + */ + description?: undefined; + /** + * @deprecated Only for type compatibility with @vscode/js-debug + */ + objectId?: undefined; + /** + * @deprecated Only for type compatibility with @vscode/js-debug + */ + linearMemoryAddress?: undefined; + /** + * @deprecated Only for type compatibility with @vscode/js-debug + */ + linearMemorySize?: undefined; } export interface PropertyDescriptor { name: string; - value: RemoteObject|ForeignObject; + value: RemoteObject | ForeignObject; } export interface LanguageExtensionPlugin { @@ -213,8 +99,8 @@ export namespace Chrome { * A new raw module has been loaded. If the raw wasm module references an external debug info module, its URL will be * passed as symbolsURL. */ - addRawModule(rawModuleId: string, symbolsURL: string|undefined, rawModule: RawModule): - Promise; + addRawModule(rawModuleId: string, symbolsURL: string | undefined, rawModule: RawModule): + Promise; /** * Find locations in raw modules from a location in a source file. @@ -246,8 +132,8 @@ export namespace Chrome { * the location is inside of an inlined function with the innermost function at index 0. */ getFunctionInfo(rawLocation: RawLocation): - Promise<{frames: Array, missingSymbolFiles: Array}|{missingSymbolFiles: Array}| - {frames: Array}>; + Promise<{ frames: Array, missingSymbolFiles: Array; } | { missingSymbolFiles: Array; } | + { frames: Array; }>; /** * Find locations in raw modules corresponding to the inline function @@ -265,14 +151,14 @@ export namespace Chrome { /** * Retrieve a list of line numbers in a file for which line-to-raw-location mappings exist. */ - getMappedLines(rawModuleId: string, sourceFileURL: string): Promise; + getMappedLines(rawModuleId: string, sourceFileURL: string): Promise; /** * Evaluate a source language expression in the context of a given raw location and a given stopId. stopId is an * opaque key that should be passed to the APIs accessing wasm state, e.g., getWasmLinearMemory. A stopId is * invalidated once the debugger resumes. */ - evaluate(expression: string, context: RawLocation, stopId: unknown): Promise; + evaluate(expression: string, context: RawLocation, stopId: unknown): Promise; /** * Retrieve properties of the remote object identified by the object id. @@ -285,62 +171,10 @@ export namespace Chrome { releaseObject(objectId: RemoteObjectId): Promise; } - - export interface SupportedScriptTypes { - language: string; - symbol_types: string[]; - } - - export type WasmValue = {type: 'i32'|'f32'|'f64', value: number}|{type: 'i64', value: bigint}| - {type: 'v128', value: string}|ForeignObject; - - export interface LanguageExtensions { - registerLanguageExtensionPlugin( - plugin: LanguageExtensionPlugin, pluginName: string, - supportedScriptTypes: SupportedScriptTypes): Promise; - unregisterLanguageExtensionPlugin(plugin: LanguageExtensionPlugin): Promise; - - getWasmLinearMemory(offset: number, length: number, stopId: unknown): Promise; - getWasmLocal(local: number, stopId: unknown): Promise; - getWasmGlobal(global: number, stopId: unknown): Promise; - getWasmOp(op: number, stopId: unknown): Promise; - - reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): - Promise; - } - - export interface Position { - line: number; - column: number; - } - - export interface NamedFunctionRange { - readonly name: string; - readonly start: Position; - readonly end: Position; - } - - export interface RecorderExtensions { - registerRecorderExtensionPlugin(plugin: RecorderExtensionPlugin, pluginName: string, mediaType?: string): - Promise; - unregisterRecorderExtensionPlugin(plugin: RecorderExtensionPlugin): Promise; - createView(title: string, pagePath: string): Promise; - } - - export interface Performance { - onProfilingStarted: EventSink<() => unknown>; - onProfilingStopped: EventSink<() => unknown>; - } - - export interface Chrome { - devtools: DevToolsAPI; - experimental: {devtools: ExperimentalDevToolsAPI}; - } - } -} - -declare global { - interface Window { - chrome: Chrome.DevTools.Chrome; + export type WasmValue = + | { type: 'i32' | 'f32' | 'f64', value: number; } + | { type: 'i64', value: bigint; } + | { type: 'v128', value: string; } + | ForeignObject; } } diff --git a/src/Formatters.ts b/src/Formatters.ts index d4e1b74..74a7aca 100644 --- a/src/Formatters.ts +++ b/src/Formatters.ts @@ -10,23 +10,23 @@ import { type TypeInfo, type Value, type WasmInterface, -} from './CustomFormatters.js'; -import type {ForeignObject} from './WasmTypes.js'; +} from './CustomFormatters'; +import type { ForeignObject } from './WasmTypes'; /* * Numbers */ -CustomFormatters.addFormatter({types: ['bool'], format: (wasm, value) => value.asUint8() > 0}); -CustomFormatters.addFormatter({types: ['uint16_t'], format: (wasm, value) => value.asUint16()}); -CustomFormatters.addFormatter({types: ['uint32_t'], format: (wasm, value) => value.asUint32()}); -CustomFormatters.addFormatter({types: ['uint64_t'], format: (wasm, value) => value.asUint64()}); +CustomFormatters.addFormatter({ types: ['bool'], format: (wasm, value) => value.asUint8() > 0 }); +CustomFormatters.addFormatter({ types: ['uint16_t'], format: (wasm, value) => value.asUint16() }); +CustomFormatters.addFormatter({ types: ['uint32_t'], format: (wasm, value) => value.asUint32() }); +CustomFormatters.addFormatter({ types: ['uint64_t'], format: (wasm, value) => value.asUint64() }); -CustomFormatters.addFormatter({types: ['int16_t'], format: (wasm, value) => value.asInt16()}); -CustomFormatters.addFormatter({types: ['int32_t'], format: (wasm, value) => value.asInt32()}); -CustomFormatters.addFormatter({types: ['int64_t'], format: (wasm, value) => value.asInt64()}); +CustomFormatters.addFormatter({ types: ['int16_t'], format: (wasm, value) => value.asInt16() }); +CustomFormatters.addFormatter({ types: ['int32_t'], format: (wasm, value) => value.asInt32() }); +CustomFormatters.addFormatter({ types: ['int64_t'], format: (wasm, value) => value.asInt64() }); -CustomFormatters.addFormatter({types: ['float'], format: (wasm, value) => value.asFloat32()}); -CustomFormatters.addFormatter({types: ['double'], format: (wasm, value) => value.asFloat64()}); +CustomFormatters.addFormatter({ types: ['float'], format: (wasm, value) => value.asFloat32() }); +CustomFormatters.addFormatter({ types: ['double'], format: (wasm, value) => value.asFloat64() }); export const enum Constants { MAX_STRING_LEN = (1 << 28) - 16, // This is the maximum string len for 32bit taken from V8 @@ -37,11 +37,11 @@ export function formatVoid(): () => LazyObject { return () => new PrimitiveLazyObject('undefined', undefined, ''); } -CustomFormatters.addFormatter({types: ['void'], format: formatVoid}); +CustomFormatters.addFormatter({ types: ['void'], format: formatVoid }); -CustomFormatters.addFormatter({types: ['uint8_t', 'int8_t'], format: formatChar}); +CustomFormatters.addFormatter({ types: ['uint8_t', 'int8_t'], format: formatChar }); -export function formatChar(wasm: WasmInterface, value: Value): string { +export function formatChar(wasm: WasmInterface, value: Value): string | number { const char = value.typeNames.includes('int8_t') ? Math.abs(value.asInt8()) : value.asUint8(); switch (char) { case 0x0: @@ -60,6 +60,8 @@ export function formatChar(wasm: WasmInterface, value: Value): string { return '\'\\f\''; case 0xD: return '\'\\r\''; + case 0x27: + return '\'\\\'\''; } if (char < 0x20 || char > 0x7e) { return `'\\x${char.toString(16).padStart(2, '0')}'`; @@ -79,15 +81,29 @@ CustomFormatters.addFormatter({ }, }); +interface FormattedStringResult = Uint8Array> { + size: number; + string: string; + chars: T; +} + +function formattedStringResult>(chars: T, decode: (chars: T) => string): FormattedStringResult { + return { + size: chars.length, + string: JSON.stringify(decode(chars)), + chars + }; +} + /* * STL */ function formatLibCXXString( - wasm: WasmInterface, value: Value, charType: T, - decode: (chars: InstanceType) => string): {size: number, string: string} { + wasm: WasmInterface, value: Value, charType: T, + decode: (chars: InstanceType) => string): FormattedStringResult> { const shortString = value.$('__r_.__value_..__s'); const size = shortString.getMembers().includes('') ? shortString.$('.__size_').asUint8() : - shortString.$('__size_').asUint8(); + shortString.$('__size_').asUint8(); const isLong = 0 < (size & 0x80); const charSize = charType.BYTES_PER_ELEMENT; if (isLong) { @@ -98,26 +114,26 @@ function formatLibCXXString( const copyLen = Math.min(stringSize * charSize, Constants.MAX_STRING_LEN); const bytes = wasm.readMemory(data, copyLen); const text = new charType(bytes.buffer, bytes.byteOffset, stringSize) as InstanceType; - return {size: stringSize, string: decode(text)}; + return formattedStringResult(text, decode); } const bytes = shortString.$('__data_').asDataView(0, size * charSize); const text = new charType(bytes.buffer, bytes.byteOffset, size) as InstanceType; - return {size, string: decode(text)}; + return formattedStringResult(text, decode); } -export function formatLibCXX8String(wasm: WasmInterface, value: Value): {size: number, string: string} { +export function formatLibCXX8String(wasm: WasmInterface, value: Value): FormattedStringResult { return formatLibCXXString(wasm, value, Uint8Array, str => new TextDecoder().decode(str)); } -export function formatLibCXX16String(wasm: WasmInterface, value: Value): {size: number, string: string} { +export function formatLibCXX16String(wasm: WasmInterface, value: Value): FormattedStringResult { return formatLibCXXString(wasm, value, Uint16Array, str => new TextDecoder('utf-16le').decode(str)); } -export function formatLibCXX32String(wasm: WasmInterface, value: Value): {size: number, string: string} { +export function formatLibCXX32String(wasm: WasmInterface, value: Value): FormattedStringResult { // emscripten's wchar is 4 byte return formatLibCXXString( - wasm, value, Uint32Array, str => Array.from(str).map(v => String.fromCodePoint(v)).join('')); + wasm, value, Uint32Array, str => Array.from(str).map(v => String.fromCodePoint(v)).join('')); } CustomFormatters.addFormatter({ @@ -148,13 +164,11 @@ CustomFormatters.addFormatter({ format: formatLibCXX32String, }); -type CharArrayConstructor = Uint8ArrayConstructor|Uint16ArrayConstructor|Uint32ArrayConstructor; +type CharArrayConstructor = Uint8ArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor; function formatRawString( - wasm: WasmInterface, value: Value, charType: T, decode: (chars: InstanceType) => string): string|{ - [key: string]: Value|null, -} -{ + wasm: WasmInterface, value: Value, charType: T, decode: (chars: InstanceType) => string): + FormattedStringResult> | { [key: string]: Value | null; } { const address = value.asUint32(); if (address < Constants.SAFE_HEAP_START) { return formatPointerOrReference(wasm, value); @@ -173,44 +187,40 @@ function formatRawString( const str = new charType(bufferSize / charSize + strlen) as InstanceType; for (let i = 0; i < slices.length; ++i) { str.set( - // @ts-expect-error TypeScript can't find the deduce the intersection type correctly - new charType(slices[i].buffer, slices[i].byteOffset, slices[i].byteLength / charSize), - i * Constants.PAGE_SIZE / charSize); + new charType(slices[i].buffer, slices[i].byteOffset, slices[i].byteLength / charSize), + i * Constants.PAGE_SIZE / charSize); } str.set(substr.subarray(0, strlen), bufferSize / charSize); - return decode(str); + return formattedStringResult(str, decode); } slices.push(buffer); } return formatPointerOrReference(wasm, value); } -export function formatCString(wasm: WasmInterface, value: Value): string|{ - [key: string]: Value|null, -} -{ +export function formatCString(wasm: WasmInterface, value: Value): FormattedStringResult | { + [key: string]: Value | null, +} { return formatRawString(wasm, value, Uint8Array, str => new TextDecoder().decode(str)); } -export function formatU16CString(wasm: WasmInterface, value: Value): string|{ - [key: string]: Value|null, -} -{ +export function formatU16CString(wasm: WasmInterface, value: Value): FormattedStringResult | { + [key: string]: Value | null, +} { return formatRawString(wasm, value, Uint16Array, str => new TextDecoder('utf-16le').decode(str)); } -export function formatCWString(wasm: WasmInterface, value: Value): string|{ - [key: string]: Value|null, -} -{ +export function formatCWString(wasm: WasmInterface, value: Value): FormattedStringResult | { + [key: string]: Value | null, +} { // emscripten's wchar is 4 byte return formatRawString(wasm, value, Uint32Array, str => Array.from(str).map(v => String.fromCodePoint(v)).join('')); } // Register with higher precedence than the generic pointer handler. -CustomFormatters.addFormatter({types: ['char *', 'char8_t *'], format: formatCString}); -CustomFormatters.addFormatter({types: ['char16_t *'], format: formatU16CString}); -CustomFormatters.addFormatter({types: ['wchar_t *', 'char32_t *'], format: formatCWString}); +CustomFormatters.addFormatter({ types: ['char *', 'char8_t *'], format: formatCString }); +CustomFormatters.addFormatter({ types: ['char16_t *'], format: formatU16CString }); +CustomFormatters.addFormatter({ types: ['wchar_t *', 'char32_t *'], format: formatCWString }); export function formatVector(wasm: WasmInterface, value: Value): Value[] { const begin = value.$('__begin_'); @@ -246,46 +256,46 @@ function reMatch(...exprs: RegExp[]): (type: TypeInfo) => boolean { }; } -CustomFormatters.addFormatter({types: reMatch(/^std::vector<.+>$/), format: formatVector}); +CustomFormatters.addFormatter({ types: reMatch(/^std::vector<.+>$/), format: formatVector }); -export function formatPointerOrReference(wasm: WasmInterface, value: Value): {[key: string]: Value|null} { +export function formatPointerOrReference(wasm: WasmInterface, value: Value): { [key: string]: Value | null; } { const address = value.asUint32(); if (address === 0) { - return {'0x0': null}; + return { '0x0': null }; } - return {[`0x${address.toString(16)}`]: value.$('*')}; + return { [`0x${address.toString(16)}`]: value.$('*') }; } -CustomFormatters.addFormatter({types: type => type.isPointer, format: formatPointerOrReference}); +CustomFormatters.addFormatter({ types: type => type.isPointer, format: formatPointerOrReference }); -export function formatDynamicArray(wasm: WasmInterface, value: Value): {[key: string]: Value|null} { - return {[`0x${value.location.toString(16)}`]: value.$(0)}; +export function formatDynamicArray(wasm: WasmInterface, value: Value): { [key: string]: Value | null; } { + return { [`0x${value.location.toString(16)}`]: value.$(0) }; } -CustomFormatters.addFormatter({types: reMatch(/^.+\[\]$/), format: formatDynamicArray}); +CustomFormatters.addFormatter({ types: reMatch(/^.+\[\]$/), format: formatDynamicArray }); export function formatUInt128(wasm: WasmInterface, value: Value): bigint { const view = value.asDataView(); return (view.getBigUint64(8, true) << BigInt(64)) + (view.getBigUint64(0, true)); } -CustomFormatters.addFormatter({types: ['unsigned __int128'], format: formatUInt128}); +CustomFormatters.addFormatter({ types: ['unsigned __int128'], format: formatUInt128 }); export function formatInt128(wasm: WasmInterface, value: Value): bigint { const view = value.asDataView(); return (view.getBigInt64(8, true) << BigInt(64)) | (view.getBigUint64(0, true)); } -CustomFormatters.addFormatter({types: ['__int128'], format: formatInt128}); +CustomFormatters.addFormatter({ types: ['__int128'], format: formatInt128 }); export function formatExternRef(wasm: WasmInterface, value: Value): () => LazyObject { const obj = { - async getProperties(): Promise> { + async getProperties(): Promise> { return []; }, async asRemoteObject(): Promise { const encodedValue = value.asUint64(); const ValueClasses: ['global', 'local', 'operand'] = ['global', 'local', 'operand']; const valueClass = ValueClasses[Number(encodedValue >> 32n)]; - return {type: 'reftype', valueClass, index: Number(BigInt.asUintN(32, encodedValue))}; + return { type: 'reftype', valueClass, index: Number(BigInt.asUintN(32, encodedValue)) }; } }; return () => obj; } -CustomFormatters.addFormatter({types: ['__externref_t', 'externref_t'], format: formatExternRef}); +CustomFormatters.addFormatter({ types: ['__externref_t', 'externref_t'], format: formatExternRef }); diff --git a/src/PathUtils.ts b/src/PathUtils.ts index c9aef82..38e21ae 100644 --- a/src/PathUtils.ts +++ b/src/PathUtils.ts @@ -3,24 +3,6 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/ModuleConfiguration.ts -import {globMatch} from './GlobMatch.js'; - -/** - * A path substitution specifies a string prefix pattern to be - * replaced with a new string. This is the pendant of the - * `set substitute-path old new` feature that is found in GDB - * and `settings set target.source-map old new` feature that - * is found in LLDB. - */ -export interface PathSubstitution { - readonly from: string; readonly to: string; -} - -/** - * List of {@type PathSubstitution}s. - */ -export type PathSubstitutions = readonly PathSubstitution[]; - /** * Resolve a source path (as stored in DWARF debugging information) to an absolute URL. * @@ -29,34 +11,13 @@ export type PathSubstitutions = readonly PathSubstitution[]; * LLDB frontend in `PathMappingList::RemapPath()` inside `Target/PathMappingList.cpp` * (http://cs/github/llvm/llvm-project/lldb/source/Target/PathMappingList.cpp?l=157-185). * - * @param pathSubstitutions possible substitutions to apply to the {@param sourcePath}, applies the first match. * @param sourcePath the source path as found in the debugging information. * @param baseURL the URL of the WebAssembly module, which is used to resolve relative source paths. * @return an absolute `file:`-URI or a URL relative to the {@param baseURL}. */ -export function resolveSourcePathToURL(pathSubstitutions: PathSubstitutions, sourcePath: string, baseURL: URL): URL { +export function resolveSourcePathToURL(sourcePath: string, baseURL: URL): URL { // Normalize '\' to '/' in sourcePath first. - let resolvedSourcePath = sourcePath.replace(/\\/g, '/'); - - // Apply source path substitutions first. - for (const {from, to} of pathSubstitutions) { - if (resolvedSourcePath.startsWith(from)) { - resolvedSourcePath = to + resolvedSourcePath.slice(from.length); - break; - } - - // Relative paths won't have a leading "./" in them unless "." is the only - // thing in the relative path so we need to work around "." carefully. - if (from === '.') { - // We need to figure whether sourcePath can be considered a relative path, - // ruling out absolute POSIX and Windows paths, as well as file:, http: and - // https: URLs. - if (!resolvedSourcePath.startsWith('/') && !/^([A-Z]|file|https?):/i.test(resolvedSourcePath)) { - resolvedSourcePath = `${to}/${resolvedSourcePath}`; - break; - } - } - } + const resolvedSourcePath = sourcePath.replace(/\\/g, '/'); if (resolvedSourcePath.startsWith('/')) { if (resolvedSourcePath.startsWith('//')) { @@ -69,50 +30,3 @@ export function resolveSourcePathToURL(pathSubstitutions: PathSubstitutions, sou } return new URL(resolvedSourcePath, baseURL.href); } - -/** - * Configuration for locating source files for a given WebAssembly module. - * If the name is `undefined`, the configuration is the default configuration, - * which is chosen if there's no named configuration matching the basename of - * the WebAssembly module file. - * The name can be a wildcard pattern, and {@see globMatch} will be used to - * match the name against the URL of the WebAssembly module file. - */ -export interface ModuleConfiguration { - readonly name?: string; readonly pathSubstitutions: PathSubstitutions; -} - -/** - * List of {@type ModuleConfiguration}s. These lists are intended to have - * a default configuration, whose name field is `undefined`, which is chosen - * when no matching named configuration is found. - */ -export type ModuleConfigurations = readonly ModuleConfiguration[]; - -/** - * Locate the configuration for a given `something.wasm` module file name. - * - * @param moduleConfigurations list of module configurations to scan. - * @param moduleName the URL of the module to lookup. - * @return the matching module configuration or the default fallback. - */ -export function findModuleConfiguration( - moduleConfigurations: ModuleConfigurations, moduleURL: URL): ModuleConfiguration { - let defaultModuleConfiguration: ModuleConfiguration = {pathSubstitutions: []}; - for (const moduleConfiguration of moduleConfigurations) { - // The idea here is that module configurations will have at most - // one default configuration, so picking the last here is fine. - if (moduleConfiguration.name === undefined) { - defaultModuleConfiguration = moduleConfiguration; - continue; - } - - // Perform wildcard pattern matching on the full URL. - if (globMatch(moduleConfiguration.name, moduleURL.href)) { - return moduleConfiguration; - } - } - return defaultModuleConfiguration; -} - -export const DEFAULT_MODULE_CONFIGURATIONS: ModuleConfigurations = [{pathSubstitutions: []}]; diff --git a/src/RPCInterface.ts b/src/RPCInterface.ts index 35b2769..8dd8702 100644 --- a/src/RPCInterface.ts +++ b/src/RPCInterface.ts @@ -3,12 +3,11 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/DevToolsPluginWorker.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import type { Chrome } from './ExtensionAPI'; -import {createPlugin, type ResourceLoader} from './DWARFSymbols.js'; -import type {ModuleConfigurations} from './ModuleConfiguration.js'; -import {deserializeWasmMemory, deserializeWasmValue, kMaxWasmValueSize, type WasmValue} from './WasmTypes.js'; -import {type Channel, type HostInterface, SynchronousIOMessage, type WorkerInterface, WorkerRPC} from './WorkerRPC.js'; +import { createPlugin, type ResourceLoader } from './DWARFLanguageExtensionPlugin'; +import { deserializeWasmMemory, deserializeWasmValue, kMaxWasmValueSize, type WasmValue } from './WasmTypes'; +import { type Channel, type HostInterface, SynchronousIOMessage, type WorkerInterface, WorkerRPC } from './WorkerRPC'; class SynchronousLinearMemoryMessage extends SynchronousIOMessage { deserialize(length: number): ArrayBuffer { @@ -44,25 +43,25 @@ export class RPCInterface implements WorkerInterface, HostInterface { getWasmLinearMemory(offset: number, length: number, stopId: unknown): ArrayBuffer { return this.rpc.sendMessageSync( - new SynchronousLinearMemoryMessage(length), 'getWasmLinearMemory', offset, length, stopId); + new SynchronousLinearMemoryMessage(length), 'getWasmLinearMemory', offset, length, stopId); } getWasmLocal(local: number, stopId: unknown): WasmValue { return this.rpc.sendMessageSync(new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmLocal', local, stopId); } getWasmGlobal(global: number, stopId: unknown): WasmValue { return this.rpc.sendMessageSync( - new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmGlobal', global, stopId); + new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmGlobal', global, stopId); } getWasmOp(op: number, stopId: unknown): WasmValue { return this.rpc.sendMessageSync(new SynchronousWasmValueMessage(kMaxWasmValueSize), 'getWasmOp', op, stopId); } - reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): - Promise { + reportResourceLoad(resourceUrl: string, status: { success: boolean, errorMessage?: string, size?: number; }): + Promise { return this.rpc.sendMessage('reportResourceLoad', resourceUrl, status); } evaluate(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): - Promise { + Promise { if (this.plugin.evaluate) { return this.plugin.evaluate(expression, context, stopId); } @@ -81,12 +80,12 @@ export class RPCInterface implements WorkerInterface, HostInterface { return Promise.resolve(); } - addRawModule(rawModuleId: string, symbolsURL: string|undefined, rawModule: Chrome.DevTools.RawModule): - Promise { + addRawModule(rawModuleId: string, symbolsURL: string | undefined, rawModule: Chrome.DevTools.RawModule): + Promise { return this.plugin.addRawModule(rawModuleId, symbolsURL, rawModule); } sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): - Promise { + Promise { return this.plugin.sourceLocationToRawLocation(sourceLocation); } rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): Promise { @@ -102,8 +101,8 @@ export class RPCInterface implements WorkerInterface, HostInterface { return this.plugin.removeRawModule(rawModuleId); } getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): - Promise<{frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]}| - {frames: Chrome.DevTools.FunctionInfo[]}|{missingSymbolFiles: string[]}> { + Promise<{ frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]; } | + { frames: Chrome.DevTools.FunctionInfo[]; } | { missingSymbolFiles: string[]; }> { return this.plugin.getFunctionInfo(rawLocation); } getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { @@ -112,10 +111,11 @@ export class RPCInterface implements WorkerInterface, HostInterface { getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { return this.plugin.getInlinedCalleesRanges(rawLocation); } - getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { + getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { return this.plugin.getMappedLines(rawModuleId, sourceFileURL); } - async hello(moduleConfigurations: ModuleConfigurations, logPluginApiCalls: boolean): Promise { - this.#plugin = await createPlugin(this, this.resourceLoader, moduleConfigurations, logPluginApiCalls); + async hello(moduleConfiguration: { logPluginApiCalls?: boolean; } | unknown[], logPluginApiCalls?: boolean): Promise { + logPluginApiCalls = (Array.isArray(moduleConfiguration) ? logPluginApiCalls : moduleConfiguration.logPluginApiCalls) || false; + this.#plugin = await createPlugin(this, this.resourceLoader, logPluginApiCalls); } } diff --git a/src/ResourceLoader.ts b/src/ResourceLoader.ts index 2cc73b9..9180dc5 100644 --- a/src/ResourceLoader.ts +++ b/src/ResourceLoader.ts @@ -3,24 +3,26 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/MEMFSResourceLoader.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import type { Chrome } from './ExtensionAPI'; -import type * as DWARFSymbols from './DWARFSymbols.js'; -import type {HostInterface} from './WorkerRPC.js'; +import type * as DWARFSymbols from './DWARFLanguageExtensionPlugin'; +import type { HostInterface } from './WorkerRPC'; export class ResourceLoader implements DWARFSymbols.ResourceLoader { protected async fetchSymbolsData(rawModule: DWARFSymbols.RawModule, url: URL, hostInterface: HostInterface): - Promise<{symbolsData: ArrayBuffer, symbolsDwpData?: ArrayBuffer}> { + Promise<{ symbolsData: ArrayBuffer, symbolsDwpData?: ArrayBuffer; }> { if (rawModule.code) { - return {symbolsData: rawModule.code, symbolsDwpData: rawModule.dwp}; + return { symbolsData: rawModule.code, symbolsDwpData: rawModule.dwp }; } - const symbolsResponse = await fetch(url.href, {mode: 'no-cors'}); + const symbolsResponse = await fetch(url.href, { mode: 'no-cors' }); if (symbolsResponse.ok) { let symbolsDwpResponse = undefined; let symbolsDwpError; const dwpUrl = `${url.href}.dwp`; try { - symbolsDwpResponse = await fetch(dwpUrl, {mode: 'no-cors'}); + symbolsDwpResponse = await fetch(dwpUrl, { mode: 'no-cors' }); } catch (e) { symbolsDwpError = (e as Error).message; // Unclear if this ever happens; usually if the file isn't there we @@ -40,35 +42,37 @@ export class ResourceLoader implements DWARFSymbols.ResourceLoader { symbolsResponse.arrayBuffer(), symbolsDwpResponse?.ok ? symbolsDwpResponse.arrayBuffer() : undefined, ]); - void hostInterface.reportResourceLoad(url.href, {success: true, size: symbolsData.byteLength}); + void hostInterface.reportResourceLoad(url.href, { success: true, size: symbolsData.byteLength }); if (symbolsDwpData) { - void hostInterface.reportResourceLoad(dwpUrl, {success: true, size: symbolsDwpData.byteLength}); + void hostInterface.reportResourceLoad(dwpUrl, { success: true, size: symbolsDwpData.byteLength }); } else { void hostInterface.reportResourceLoad( - dwpUrl, {success: false, errorMessage: `Failed to fetch dwp file: ${symbolsDwpError}`}); + dwpUrl, { success: false, errorMessage: `Failed to fetch dwp file: ${symbolsDwpError}` }); } - return {symbolsData, symbolsDwpData}; + return { symbolsData, symbolsDwpData }; } const statusText = symbolsResponse.statusText || `status code ${symbolsResponse.status}`; if (rawModule.url !== url.href) { - const errorMessage = `NotFoundError: Unable to load debug symbols from '${url}' for the WebAssembly module '${ - rawModule.url}' (${statusText}), double-check the parameter to -gseparate-dwarf in your Emscripten link step`; - void hostInterface.reportResourceLoad(url.href, {success: false, errorMessage}); + const errorMessage = `NotFoundError: Unable to load debug symbols from '${url}' for the WebAssembly module '${rawModule.url}' (${statusText}), double-check the parameter to -gseparate-dwarf in your Emscripten link step`; + void hostInterface.reportResourceLoad(url.href, { success: false, errorMessage }); throw new Error(errorMessage); } const errorMessage = `NotFoundError: Unable to load debug symbols from '${url}' (${statusText})`; - void hostInterface.reportResourceLoad(url.href, {success: false, errorMessage}); + void hostInterface.reportResourceLoad(url.href, { success: false, errorMessage }); throw new Error(errorMessage); } protected getModuleFileName(rawModuleId: string): string { - return `${self.btoa(rawModuleId)}.wasm`.replace(/\//g, '_'); + return `${Buffer.from(rawModuleId).toString("base64")}.wasm`.replace( + /\//g, + "_" + ); } async loadSymbols( - rawModuleId: string, rawModule: Chrome.DevTools.RawModule, symbolsURL: URL, fileSystem: typeof FS, - hostInterface: HostInterface): Promise<{symbolsFileName: string, symbolsDwpFileName: string|undefined}> { - const {symbolsData, symbolsDwpData} = await this.fetchSymbolsData(rawModule, symbolsURL, hostInterface); + rawModuleId: string, rawModule: Chrome.DevTools.RawModule, symbolsURL: URL, fileSystem: typeof FS, + hostInterface: HostInterface): Promise<{ symbolsFileName: string, symbolsDwpFileName: string | undefined; }> { + const { symbolsData, symbolsDwpData } = await this.fetchSymbolsData(rawModule, symbolsURL, hostInterface); const symbolsFileName = this.getModuleFileName(rawModuleId); const symbolsDwpFileName = symbolsDwpData && `${symbolsFileName}.dwp`; @@ -79,24 +83,19 @@ export class ResourceLoader implements DWARFSymbols.ResourceLoader { } fileSystem.createDataFile( - '/', symbolsFileName, new Uint8Array(symbolsData), true /* canRead */, false /* canWrite */, true /* canOwn */); + '/', symbolsFileName, new Uint8Array(symbolsData), true /* canRead */, false /* canWrite */, true /* canOwn */); if (symbolsDwpData && symbolsDwpFileName) { fileSystem.createDataFile( - '/', symbolsDwpFileName, new Uint8Array(symbolsDwpData), true /* canRead */, false /* canWrite */, - true /* canOwn */); + '/', symbolsDwpFileName, new Uint8Array(symbolsDwpData), true /* canRead */, false /* canWrite */, + true /* canOwn */); } - return {symbolsFileName, symbolsDwpFileName}; + return { symbolsFileName, symbolsDwpFileName }; } - createSymbolsBackendModulePromise(): Promise { - const url = new URL('SymbolsBackend.wasm', import.meta.url); - return fetch(url.href, {credentials: 'same-origin'}).then(response => { - if (!response.ok) { - throw new Error(response.statusText); - } - return WebAssembly.compileStreaming(response); - }); + async createSymbolsBackendModulePromise(): Promise { + const file = await fs.readFile(join(__dirname, "SymbolsBackend.wasm")); + return WebAssembly.compile(file); } possiblyMissingSymbols?: string[]; diff --git a/src/SymbolsBackend.d.ts b/src/SymbolsBackend.d.ts index 8c3240b..329435d 100644 --- a/src/SymbolsBackend.d.ts +++ b/src/SymbolsBackend.d.ts @@ -16,7 +16,7 @@ export interface Vector extends EmbindObject { push_back(value: T): void; } -export interface ErrorCode extends EmbindObject {} +export interface ErrorCode extends EmbindObject { } export interface Error extends EmbindObject { code: ErrorCode; @@ -42,7 +42,7 @@ export interface SourceLocation extends EmbindObject { columnNumber: number; } -export interface VariableScope extends EmbindObject {} +export interface VariableScope extends EmbindObject { } export interface Variable extends EmbindObject { scope: VariableScope; @@ -52,7 +52,7 @@ export interface Variable extends EmbindObject { } export interface FieldInfo extends EmbindObject { - name: string|undefined; + name: string | undefined; offset: number; typeId: string; } @@ -70,7 +70,7 @@ export interface TypeInfo extends EmbindObject { size: number; canExpand: boolean; hasValue: boolean; - arraySize: number|undefined; + arraySize: number | undefined; isPointer: boolean; members: Vector; enumerators: Vector; @@ -79,62 +79,62 @@ export interface TypeInfo extends EmbindObject { export interface AddRawModuleResponse extends EmbindObject { sources: Vector; dwos: Vector; - error: Error|undefined; + error: Error | undefined; } export interface SourceLocationToRawLocationResponse extends EmbindObject { rawLocationRanges: Vector; - error: Error|undefined; + error: Error | undefined; } export interface RawLocationToSourceLocationResponse extends EmbindObject { sourceLocation: Vector; - error: Error|undefined; + error: Error | undefined; } export interface ListVariablesInScopeResponse extends EmbindObject { variable: Vector; - error: Error|undefined; + error: Error | undefined; } export interface GetFunctionInfoResponse extends EmbindObject { functionNames: Vector; missingSymbolFiles: Vector; - error: Error|undefined; + error: Error | undefined; } export interface GetInlinedFunctionRangesResponse extends EmbindObject { rawLocationRanges: Vector; - error: Error|undefined; + error: Error | undefined; } export interface GetInlinedCalleesRangesResponse extends EmbindObject { rawLocationRanges: Vector; - error: Error|undefined; + error: Error | undefined; } export interface GetMappedLinesResponse extends EmbindObject { MappedLines: Vector; - error: Error|undefined; + error: Error | undefined; } export interface EvaluateExpressionResponse extends EmbindObject { typeInfos: Vector; root: TypeInfo; - displayValue: string|undefined; - location: number|undefined; - memoryAddress: number|undefined; - data: Vector|undefined; - error: Error|undefined; + displayValue: string | undefined; + location: number | undefined; + memoryAddress: number | undefined; + data: Vector | undefined; + error: Error | undefined; } export interface DWARFSymbolsPlugin extends EmbindObject { AddRawModule(rawModuleId: string, path: string): AddRawModuleResponse; RemoveRawModule(rawModuleId: string): void; SourceLocationToRawLocation(rawModuleId: string, sourceFileURL: string, lineNumber: number, columnNumber: number): - SourceLocationToRawLocationResponse; + SourceLocationToRawLocationResponse; RawLocationToSourceLocation(rawModuleId: string, codeOffset: number, inlineFrameIndex: number): - RawLocationToSourceLocationResponse; + RawLocationToSourceLocationResponse; ListVariablesInScope(rawModuleId: string, codeOffset: number, inlineFrameIndex: number): ListVariablesInScopeResponse; GetFunctionInfo(rawModuleId: string, codeOffset: number): GetFunctionInfoResponse; GetInlinedFunctionRanges(rawModuleId: string, codeOffset: number): GetInlinedFunctionRangesResponse; diff --git a/src/WasmTypes.ts b/src/WasmTypes.ts index ea60f27..4f32e9c 100644 --- a/src/WasmTypes.ts +++ b/src/WasmTypes.ts @@ -3,7 +3,7 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/WasmTypes.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI'; +import type { Chrome } from './ExtensionAPI'; export type ForeignObject = Chrome.DevTools.ForeignObject; @@ -11,7 +11,7 @@ export type WasmValue = Chrome.DevTools.WasmValue; export type WasmSimdValue = string; -export type WasmPrimitive = number|bigint|WasmSimdValue; +export type WasmPrimitive = number | bigint | WasmSimdValue; /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/prefer-enum-initializers */ export const enum SerializedWasmType { @@ -24,7 +24,7 @@ export const enum SerializedWasmType { } /* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/prefer-enum-initializers */ -export function serializeWasmValue(value: WasmValue|ArrayBuffer, buffer: ArrayBufferLike): SerializedWasmType { +export function serializeWasmValue(value: WasmValue | ArrayBuffer, buffer: ArrayBufferLike): SerializedWasmType { if (value instanceof ArrayBuffer) { const data = new Uint8Array(value); new Uint8Array(buffer).set(data); @@ -77,13 +77,13 @@ export function deserializeWasmValue(buffer: ArrayBufferLike, type: SerializedWa const view = new DataView(buffer); switch (type) { case SerializedWasmType.i32: - return {type: 'i32', value: view.getInt32(0, true)}; + return { type: 'i32', value: view.getInt32(0, true) }; case SerializedWasmType.i64: - return {type: 'i64', value: view.getBigInt64(0, true)}; + return { type: 'i64', value: view.getBigInt64(0, true) }; case SerializedWasmType.f32: - return {type: 'f32', value: view.getFloat32(0, true)}; + return { type: 'f32', value: view.getFloat32(0, true) }; case SerializedWasmType.f64: - return {type: 'f64', value: view.getFloat64(0, true)}; + return { type: 'f64', value: view.getFloat64(0, true) }; case SerializedWasmType.v128: { const a = view.getUint32(0, true); const b = view.getUint32(4, true); @@ -91,15 +91,14 @@ export function deserializeWasmValue(buffer: ArrayBufferLike, type: SerializedWa const d = view.getUint32(12, true); return { type: 'v128', - value: `i32x4 0x${a.toString(16).padStart(8, '0')} 0x${b.toString(16).padStart(8, '0')} 0x${ - c.toString(16).padStart(8, '0')} 0x${d.toString(16).padStart(8, '0')}` + value: `i32x4 0x${a.toString(16).padStart(8, '0')} 0x${b.toString(16).padStart(8, '0')} 0x${c.toString(16).padStart(8, '0')} 0x${d.toString(16).padStart(8, '0')}` }; } case SerializedWasmType.reftype: { const ValueClasses: ['local', 'global', 'operand'] = ['local', 'global', 'operand']; const valueClass = ValueClasses[view.getUint8(0)]; const index = view.getUint32(1, true); - return {type: 'reftype', valueClass, index}; + return { type: 'reftype', valueClass, index }; } } // @ts-expect-error @@ -112,9 +111,9 @@ export type WasmFunction = (...args: WasmPrimitive[]) => WasmPrimitive; export type WasmExport = { name: string, -}&({func: number}|{table: number}|{mem: number}|{global: number}); +} & ({ func: number; } | { table: number; } | { mem: number; } | { global: number; }); export type WasmImport = { name: string, module: string, -}&({func: number}|{table: number}|{mem: number}|{global: number}); +} & ({ func: number; } | { table: number; } | { mem: number; } | { global: number; }); diff --git a/src/WorkerRPC.ts b/src/WorkerRPC.ts index 1e74ffe..250772a 100644 --- a/src/WorkerRPC.ts +++ b/src/WorkerRPC.ts @@ -3,13 +3,16 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/src/WorkerRPC.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; +import type { Chrome } from './ExtensionAPI'; -import type {ModuleConfigurations} from './ModuleConfiguration.js'; -import {type SerializedWasmType, serializeWasmValue, type WasmValue} from './WasmTypes.js'; +import { type SerializedWasmType, serializeWasmValue, type WasmValue } from './WasmTypes'; export interface WorkerInterface extends Chrome.DevTools.LanguageExtensionPlugin { - hello(moduleConfigurations: ModuleConfigurations, logPluginApiCalls: boolean): void; + hello(moduleConfiguration: { logPluginApiCalls?: boolean; }): void; + /** + * @deprecated Only for compatibility with @vscode/js-debug + */ + hello(moduleConfigurations: unknown[], logPluginApiCalls: boolean): void; } export interface AsyncHostInterface { @@ -17,8 +20,8 @@ export interface AsyncHostInterface { getWasmLocal(local: number, stopId: unknown): Promise; getWasmGlobal(global: number, stopId: unknown): Promise; getWasmOp(op: number, stopId: unknown): Promise; - reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): - Promise; + reportResourceLoad(resourceUrl: string, status: { success: boolean, errorMessage?: string, size?: number; }): + Promise; } export interface HostInterface { @@ -26,23 +29,23 @@ export interface HostInterface { getWasmLocal(local: number, stopId: unknown): WasmValue; getWasmGlobal(global: number, stopId: unknown): WasmValue; getWasmOp(op: number, stopId: unknown): WasmValue; - reportResourceLoad(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}): - Promise; + reportResourceLoad(resourceUrl: string, status: { success: boolean, errorMessage?: string, size?: number; }): + Promise; } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ type AllMessages> = { - [k in keyof Interface]: {method: k, params: Parameters} + [k in keyof Interface]: { method: k, params: Parameters; } }; type Message = { requestId: number, -}&({request: AllMessages[keyof AllMessages]}|{ +} & ({ request: AllMessages[keyof AllMessages]; } | { /* eslint-disable-next-line @typescript-eslint/naming-convention */ sync_request: { request: AllMessages[keyof AllMessages], /* eslint-disable-next-line @typescript-eslint/naming-convention */ - io_buffer: {semaphore: SharedArrayBuffer, data: SharedArrayBuffer}, + io_buffer: { semaphore: SharedArrayBuffer, data: SharedArrayBuffer; }, }, }); @@ -50,7 +53,7 @@ export interface Channel|Message*/ any): any; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - onmessage: ((e: MessageEvent|Response>) => any)|null; + onmessage: ((e: MessageEvent | Response>) => any) | null; } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -60,7 +63,7 @@ type AllResponses> = { type Response = { requestId: number, -}&({error: string}|{response: AllResponses[keyof AllResponses]}); +} & ({ error: string; } | { response: AllResponses[keyof AllResponses]; }); export abstract class SynchronousIOMessage { readonly buffer: SharedArrayBuffer; @@ -70,7 +73,7 @@ export abstract class SynchronousIOMessage { abstract deserialize(response: number): T; - static serialize(value: ArrayBuffer|WasmValue, buffer: SharedArrayBuffer): SerializedWasmType { + static serialize(value: ArrayBuffer | WasmValue, buffer: SharedArrayBuffer): SerializedWasmType { return serializeWasmValue(value, buffer); } } @@ -80,7 +83,7 @@ export class WorkerRPC, RemoteInterfa private nextRequestId = 0; private readonly channel: Channel; private readonly localHandler: LocalInterface; - private readonly requests = new Map void, reject: (message: Error) => void}>(); + private readonly requests = new Map void, reject: (message: Error) => void; }>(); private readonly semaphore: Int32Array; constructor(channel: Channel, localHandler: LocalInterface) { @@ -91,25 +94,25 @@ export class WorkerRPC, RemoteInterfa } sendMessage(method: Method, ...params: Parameters): - ReturnType { + ReturnType { const requestId = this.nextRequestId++; const promise = new Promise((resolve, reject) => { - this.requests.set(requestId, {resolve, reject}); + this.requests.set(requestId, { resolve, reject }); }); - this.channel.postMessage({requestId, request: {method, params}}); + this.channel.postMessage({ requestId, request: { method, params } }); return promise as ReturnType; } sendMessageSync( - message: SynchronousIOMessage>, method: Method, - ...params: Parameters): ReturnType { + message: SynchronousIOMessage>, method: Method, + ...params: Parameters): ReturnType { const requestId = this.nextRequestId++; Atomics.store(this.semaphore, 0, 0); this.channel.postMessage({ requestId, sync_request: { - request: {method, params}, - io_buffer: {semaphore: this.semaphore.buffer as SharedArrayBuffer, data: message.buffer}, + request: { method, params }, + io_buffer: { semaphore: this.semaphore.buffer as SharedArrayBuffer, data: message.buffer }, }, }); while (Atomics.wait(this.semaphore, 0, 0) !== 'not-equal') { @@ -121,18 +124,18 @@ export class WorkerRPC, RemoteInterfa } private async onmessage( - event: MessageEvent|Message|Response>): Promise { + event: MessageEvent | Message | Response>): Promise { if ('request' in event.data) { - const {requestId, request} = event.data; + const { requestId, request } = event.data; try { const response = await this.localHandler[request.method](...request.params); - this.channel.postMessage({requestId, response}); + this.channel.postMessage({ requestId, response }); } catch (error) { - this.channel.postMessage({requestId, error: `${error}`}); + this.channel.postMessage({ requestId, error: `${error}` }); } } else if ('sync_request' in event.data) { /* eslint-disable-next-line @typescript-eslint/naming-convention */ - const {sync_request: {request, io_buffer}} = event.data; + const { sync_request: { request, io_buffer } } = event.data; let signal = -1; try { const response = await this.localHandler[request.method](...request.params); @@ -145,10 +148,10 @@ export class WorkerRPC, RemoteInterfa Atomics.notify(semaphore, 0); } } else { - const {requestId} = event.data; + const { requestId } = event.data; const callbacks = this.requests.get(requestId); if (callbacks) { - const {resolve, reject} = callbacks; + const { resolve, reject } = callbacks; if ('error' in event.data) { reject(new Error(event.data.error)); } else { diff --git a/src/index.ts b/src/index.ts index 21dff5b..9f3ff05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,9 @@ import { Channel, WorkerInterface, WorkerRPC, -} from '../chrome-cxx/mnt/extension/WorkerRPC'; +} from './WorkerRPC'; -export type * from '../chrome-cxx/mnt/extension/WorkerRPC'; +export type * from './WorkerRPC'; /** Utility type to get the worker's return value from a method name. */ export type MethodReturn = ReturnType< @@ -21,7 +21,19 @@ export interface IWasmWorker { dispose(): Promise; } -export function spawn(hostInterface: AsyncHostInterface): IWasmWorker { +export type WithOptionalMethods = T | Pick>; + +export function spawn(hostInterface: WithOptionalMethods): IWasmWorker { + + // The ms-vscode.js-debug extension currently invokes spawn without + // the reportResourceLoad method which causes the worker to fail. + if (!('reportResourceLoad' in hostInterface)) { + hostInterface = { + ...hostInterface, + async reportResourceLoad(_resourceUrl, _status) { } + }; + } + const worker = new Worker(join(__dirname, 'worker.js')); worker.on('message', (data) => channel.onmessage?.(new MessageEvent('message', { data }))); diff --git a/src/inject.ts b/src/inject.ts deleted file mode 100644 index f7df8f8..0000000 --- a/src/inject.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pathToFileURL } from "url"; - -export const import_meta_url = pathToFileURL(__filename); diff --git a/src/worker.ts b/src/worker.ts index 9b03c0a..d92de30 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,14 +1,13 @@ -import { promises as fs } from "fs"; -import { join } from "path"; -import { fileURLToPath } from "url"; -import { parentPort } from "worker_threads"; -import { RPCInterface } from "../chrome-cxx/mnt/extension/DevToolsPluginWorker"; -import { ResourceLoader } from "../chrome-cxx/mnt/extension/MEMFSResourceLoader"; +import { promises as fs } from 'fs'; +import { fileURLToPath } from 'url'; +import { parentPort } from 'worker_threads'; +import { ResourceLoader } from './ResourceLoader'; +import { RPCInterface } from './RPCInterface'; import { Channel, HostInterface, WorkerInterface, -} from "../chrome-cxx/mnt/extension/WorkerRPC"; +} from './WorkerRPC'; enableFetchToLoadFileUris(); init(); @@ -32,20 +31,7 @@ function init() { new RPCInterface( channel, - new (class extends ResourceLoader { - protected override getModuleFileName(rawModuleId: string): string { - // same logic as the parent method, but btoa doesn't exist in node - return `${Buffer.from(rawModuleId).toString("base64")}.wasm`.replace( - /\//g, - "_" - ); - } - - override async createSymbolsBackendModulePromise(): Promise { - const file = await fs.readFile(join(__dirname, "SymbolsBackend.wasm")); - return WebAssembly.compile(file); - } - })() + new ResourceLoader() ); } @@ -56,7 +42,7 @@ function init() { */ function enableFetchToLoadFileUris() { const originalFetch = globalThis.fetch; - globalThis.fetch = async (...args) => { + globalThis.fetch = async (...args: Parameters) => { const url = args[0]; if (typeof url !== "string" || !url.startsWith("file:///")) { return originalFetch(...args); @@ -89,11 +75,12 @@ function enableFetchToLoadFileUris() { blob: () => { throw new Error("not implemented"); }, + bytes: () => Promise.resolve(new Uint8Array(contents)), formData: () => { throw new Error("not implemented"); }, json: () => Promise.resolve(JSON.parse(contents.toString())), - arrayBuffer: () => Promise.resolve(contents), + arrayBuffer: () => Promise.resolve(contents.buffer as ArrayBuffer) }; return response; diff --git a/test-utils/DebuggerSession.ts b/test-utils/DebuggerSession.ts new file mode 100644 index 0000000..a694c40 --- /dev/null +++ b/test-utils/DebuggerSession.ts @@ -0,0 +1,527 @@ +import assert from 'assert'; +import { BinaryLike, createHash, randomBytes, randomUUID } from 'crypto'; +import Protocol from 'devtools-protocol'; +import ProtocolMapping from 'devtools-protocol/types/protocol-mapping'; +import EventEmitter from 'events'; +import { readFile } from 'fs/promises'; +import path from 'path'; +import { WebSocket } from 'ws'; +import { spawn } from '../dist'; +import { Chrome } from '../src/ExtensionAPI'; +import { resolveSourcePathToURL } from '../src/PathUtils'; +import { ForeignObject } from '../src/WasmTypes'; +import { createSyncInterface, SyncInterface } from './sync-interface'; + +export const TIMEOUT_IN_SECONDS = 10; +export const TIMEOUT_IN_MILLISECONDS = TIMEOUT_IN_SECONDS * 1000; + +export const BREAK_ON_START_REASON = 'Break on start' as Protocol.Debugger.PausedEvent['reason']; + +const breakOnWasm = defineEvalScript(function () { + for (const fn of [ + 'instantiate', + 'instantiateStreaming', + 'compile', + 'compileStreaming', + ] satisfies (keyof typeof WebAssembly)[]) { + const original = WebAssembly[fn]; + WebAssembly[fn] = function (...args: any[]) { + return original.apply(this, args).then((r: any) => { + // note: instantiating an existing module won't (re)compile the script + // so we have no need to stop. + if (!(args[0] instanceof WebAssembly.Module)) { + debugger; + } + return r; + }); + }; + } +}); + +export default class DebuggerSession { + #rpc: DebuggerRPC; + #workerPlugin: WorkerPlugin; + #module = new Waitable<{ + scriptId: string; + codeOffset: number; + rawModuleId: string; + sourceFiles: string[]; + }>(); + #paused = new Waitable(); + #stopIdPrefix = 0; + #stopId?: string; + #breakOnWasmId?: string; + + + constructor(url: string, modulePath: string, debugSymbolsFromPath?: string) { + this.#rpc = new DebuggerRPC(url); + this.#workerPlugin = createWorkerPlugin(this.#rpc); + this.#waitForModule(modulePath, debugSymbolsFromPath); + } + + static async attach(port: number, modulePath: string, debugSymbolsFromPath?: string) { + const debuggerUrl = await resolveDebuggerUrl(`http://127.0.0.1:${port}/json`); + const debuggerSession = new DebuggerSession(debuggerUrl, modulePath, debugSymbolsFromPath); + await debuggerSession.#waitForPaused(); + return debuggerSession; + } + + static attachSync(port: number, modulePath: string, debugSymbolsFromPath?: string): SyncInterface { + const worker = createSyncInterface(__filename, 'attach', port, modulePath, debugSymbolsFromPath); + return { + addBreakpoint: (sourceFileURL, line) => worker.invoke('addBreakpoint', sourceFileURL, line), + removeBreakpoint: (breakpointId) => worker.invoke('removeBreakpoint', breakpointId), + resume: () => worker.invoke('resume'), + continueToLine: (sourceFile, line) => worker.invoke('continueToLine', sourceFile, line), + stepOver: () => worker.invoke('stepOver'), + stepInto: () => worker.invoke('stepInto'), + stepOut: () => worker.invoke('stepOut'), + waitForPaused: () => worker.invoke('waitForPaused'), + evaluate: (expression) => worker.invoke('evaluate', expression), + getProperties: (objectId) => worker.invoke('getProperties', objectId), + listVariablesInScope: () => worker.invoke('listVariablesInScope'), + dispose(): void { + worker.invoke('dispose'); + worker.dispose(); + } + }; + } + + async addBreakpoint(sourceFile: string, line: number | string): Promise { + this.#assertIsPaused(); + const location = await this.#resolveLocation(sourceFile, line); + const { breakpointId } = await this.#rpc.send('Debugger.setBreakpoint', { location }); + return breakpointId; + } + + async removeBreakpoint(breakpointId: string): Promise { + this.#assertIsPaused(); + this.#rpc.send('Debugger.removeBreakpoint', { breakpointId }); + } + + async resume(): Promise { + this.#assertIsPaused(); + this.#paused.reset(); + await this.#rpc.send(`Debugger.resume`, {}); + } + + async continueToLine(sourceFile: string, line: number | string): Promise { + this.#assertIsPaused(); + const location = await this.#resolveLocation(sourceFile, line); + this.#paused.reset(); + await this.#rpc.send(`Debugger.continueToLocation`, { location }); + } + + async stepOver(): Promise { + const skipList = await this.#getCurrentLineLocationRanges(); + this.#paused.reset(); + await this.#rpc.send(`Debugger.stepOver`, { skipList }); + } + + async stepInto(): Promise { + const skipList = await this.#getCurrentLineLocationRanges(); + this.#paused.reset(); + await this.#rpc.send(`Debugger.stepInto`, { skipList }); + } + + async stepOut(): Promise { + this.#assertIsPaused(); + this.#paused.reset(); + await this.#rpc.send(`Debugger.stepOut`, undefined); + } + + async waitForPaused(): Promise<{ + reason: string; + sourceFileURL?: string; + lineNumber?: number; + columnNumber?: number; + hitBreakpoints?: string[]; + }> { + const { reason, hitBreakpoints, rawLocation } = await this.#waitForPaused(); + if (rawLocation) { + const sourceLocations = await this.#workerPlugin.rawLocationToSourceLocation(rawLocation); + if (sourceLocations.length > 0) { + return { + reason, + sourceFileURL: sourceLocations[0].sourceFileURL, + lineNumber: sourceLocations[0].lineNumber + 1, // Lines are 0-indexed in the worker plugin + columnNumber: sourceLocations[0].columnNumber + 1, // Columns are 0-indexed in the worker plugin + hitBreakpoints, + }; + } + } + return { reason, hitBreakpoints }; + } + + async evaluate(expression: string): Promise { + const rawLocation = await this.#assertIsPausedInWebAssemblyModule(); + return await this.#workerPlugin.evaluate(expression, rawLocation, this.#stopId); + } + + async listVariablesInScope(): Promise { + const rawLocation = await this.#assertIsPausedInWebAssemblyModule(); + return await this.#workerPlugin.listVariablesInScope(rawLocation); + } + + async getProperties(objectId: Chrome.DevTools.RemoteObjectId): Promise { + return await this.#workerPlugin.getProperties(objectId); + } + + async dispose() { + await this.#workerPlugin.dispose(); + await this.#rpc.dispose(); + } + + async #waitForModule(modulePath: string, debugSymbolsFromPath?: string) { + const rawModuleId = randomUUID(); + const rawModuleByteCodeHash = sha256(await readFile(modulePath)); + const url = resolveSourcePathToURL(path.resolve(debugSymbolsFromPath || modulePath), new URL('file://')).toString(); + + const sourceFiles = await this.#workerPlugin.addRawModule(rawModuleId, undefined, { url }); + assert(Array.isArray(sourceFiles), `Missing symbols files for '${debugSymbolsFromPath || modulePath}'.`); + + const parsedScripts: Protocol.Debugger.ScriptParsedEvent[] = []; + + this.#rpc.on('Debugger.scriptParsed', async event => { + if (event.url === breakOnWasm.url) { + this.#breakOnWasmId = event.scriptId; + } else if (event.url.startsWith('wasm://')) { + parsedScripts.push(event); + } + }); + + this.#rpc.on('Debugger.paused', async event => { + let shouldAutoResume = event.reason === BREAK_ON_START_REASON; + + if (this.#isBreakOnWasm(event)) { + shouldAutoResume = true; + if (!this.#module.done) { + while (parsedScripts.length > 0) { + const { scriptId, codeOffset = 0 } = parsedScripts.pop()!; + const parsedScriptByteCodeHash = await this.#getBytecodeHashForScript(scriptId); + if (parsedScriptByteCodeHash === rawModuleByteCodeHash) { + this.#rpc.removeAllListeners('Debugger.scriptParsed'); + parsedScripts.length = 0; + shouldAutoResume = false; + this.#module.resolve({ + scriptId, + codeOffset, + rawModuleId, + sourceFiles, + }); + break; + } + } + } + } + + if (shouldAutoResume) { + await this.#rpc.send('Debugger.resume', {}); + return; + } + this.#stopId = `${this.#stopIdPrefix++}:${event.callFrames[0].callFrameId}`; + this.#paused.resolve(event); + }); + + await this.#rpc.send('Debugger.enable', {}); + await this.#rpc.send('Runtime.evaluate', { expression: breakOnWasm.expression }); + await this.#rpc.send('Runtime.runIfWaitingForDebugger', undefined); + + await this.#module.wait(); + } + + async #waitForPaused() { + const module = await this.#module.wait(); + const { reason, hitBreakpoints, callFrames: [{ location: { scriptId, columnNumber = 0 } }] } = await this.#paused.wait(); + const rawLocation = module.scriptId === scriptId + ? { + rawModuleId: module.rawModuleId, + codeOffset: columnNumber - module.codeOffset, + inlineFrameIndex: 0 + } + : undefined; + return { scriptId, reason, hitBreakpoints, rawLocation }; + } + + #isBreakOnWasm(event: Protocol.Debugger.PausedEvent) { + return this.#breakOnWasmId && event.callFrames[0].location.scriptId === this.#breakOnWasmId; + } + + async #getBytecodeHashForScript(scriptId: string): Promise { + const { bytecode } = await this.#rpc.send('Debugger.getScriptSource', { scriptId }); + assert(bytecode, `Expected parsed WebAssembly module to return bytecode for 'Debugger.getScriptSource'.`); + return sha256(Buffer.from(bytecode, 'base64')); + } + + async #resolveLocation(sourceFile: string, line: number | string): Promise { + const lineNumber = typeof line === 'string' ? await findLineNumber(sourceFile, line) : line; + + const { scriptId, codeOffset, rawModuleId, sourceFiles } = await this.#module.wait(); + const sourceFileURL = sourceFiles.find(file => file.endsWith(`/${sourceFile}`)); + assert(sourceFileURL, `Could not find debug symbols for '${sourceFile}'.`); + + const mappedLineNumber = await this.#findMappedLine(rawModuleId, sourceFileURL, lineNumber); + const rawLocationRanges = await this.#workerPlugin.sourceLocationToRawLocation({ + rawModuleId: rawModuleId, + sourceFileURL, + lineNumber: mappedLineNumber - 1, // Lines are 0-indexed in the worker plugin + columnNumber: -1 + }); + assert(rawLocationRanges.length > 0, `Line can't be resolved to raw location: ${sourceFileURL}:${lineNumber}`); + + return { + scriptId, + lineNumber: 0, + columnNumber: codeOffset + rawLocationRanges[0].startOffset + }; + } + + async #getCurrentLineLocationRanges() { + const rawLocation = await this.#assertIsPausedInWebAssemblyModule(); + const { scriptId, codeOffset } = await this.#module.wait(); + const locationRanges: Protocol.Debugger.LocationRange[] = []; + for (const sourceLocation of await this.#workerPlugin.rawLocationToSourceLocation(rawLocation)) { + for (const { startOffset, endOffset } of await this.#workerPlugin.sourceLocationToRawLocation(sourceLocation)) { + locationRanges.push({ + scriptId, + start: { lineNumber: 0, columnNumber: startOffset + codeOffset }, + end: { lineNumber: 0, columnNumber: endOffset + codeOffset } + }); + } + } + return locationRanges; + } + + async #findMappedLine(rawModuleId: string, sourceFileURL: string, lineNumber: number): Promise { + const mappedLines = await this.#workerPlugin.getMappedLines(rawModuleId, sourceFileURL) || []; + for (const mappedLineIndex of mappedLines) { + const mappedLineNumber = mappedLineIndex + 1; // Lines are 0-indexed in the worker plugin + if (mappedLineNumber >= lineNumber) { + return mappedLineNumber; + } + } + assert.fail(`Line is unmapped: ${sourceFileURL}:${lineNumber}`); + } + + #assertIsPaused(): void { + assert(this.#paused.done, 'Must be in a paused state.'); + } + + async #assertIsPausedInWebAssemblyModule(): Promise { + this.#assertIsPaused(); + const { rawLocation } = await this.#waitForPaused(); + assert(rawLocation, 'Must be paused in WebAssembly module.'); + return rawLocation; + } +} + +interface WorkerPlugin extends Chrome.DevTools.LanguageExtensionPlugin { + dispose(): Promise; +} + +function createWorkerPlugin(debuggerSessionRPC: DebuggerRPC): WorkerPlugin { + + const { rpc, dispose } = spawn({ + getWasmGlobal: (index, stopId) => loadWasmValue(`globals[${index}]`, stopId), + getWasmLocal: (index, stopId) => loadWasmValue(`locals[${index}]`, stopId), + getWasmOp: (index, stopId) => loadWasmValue(`stack[${index}]`, stopId), + getWasmLinearMemory: (offset, length, stopId) => + loadWasmValue( + `[].slice.call(new Uint8Array(memories[0].buffer, ${+offset}, ${+length}))`, + stopId, + ).then(v => new Uint8Array(v).buffer), + }); + + rpc.sendMessage('hello', [], false); + + return { + addRawModule: (...args) => rpc.sendMessage('addRawModule', ...args), + sourceLocationToRawLocation: (...args) => rpc.sendMessage('sourceLocationToRawLocation', ...args), + rawLocationToSourceLocation: (...args) => rpc.sendMessage('rawLocationToSourceLocation', ...args), + getScopeInfo: (...args) => rpc.sendMessage('getScopeInfo', ...args), + listVariablesInScope: (...args) => rpc.sendMessage('listVariablesInScope', ...args), + removeRawModule: (...args) => rpc.sendMessage('removeRawModule', ...args), + getFunctionInfo: (...args) => rpc.sendMessage('getFunctionInfo', ...args), + getInlinedFunctionRanges: (...args) => rpc.sendMessage('getInlinedFunctionRanges', ...args), + getInlinedCalleesRanges: (...args) => rpc.sendMessage('getInlinedCalleesRanges', ...args), + getMappedLines: (...args) => rpc.sendMessage('getMappedLines', ...args), + evaluate: (...args) => rpc.sendMessage('evaluate', ...args), + getProperties: (...args) => rpc.sendMessage('getProperties', ...args), + releaseObject: (...args) => rpc.sendMessage('releaseObject', ...args), + dispose + }; + + async function loadWasmValue(expression: string, stopId: unknown) { + const stopIdString = stopId as string; + const callFrameId = stopIdString.substring(stopIdString.indexOf(':') + 1); + + const result = await debuggerSessionRPC.send('Debugger.evaluateOnCallFrame', { + callFrameId, + expression, + silent: true, + returnByValue: true, + throwOnSideEffect: true, + }); + + if (!result || result.exceptionDetails) { + throw new Error(`evaluate failed: ${result?.exceptionDetails?.text || 'unknown'}`); + } + + return result.result.value; + } +} + +function resolveDebuggerUrl(url: string) { + return retryableOperation( + async () => { + try { + const response = await fetch(url, {}).then(r => r.json()); + if (Array.isArray(response) && typeof response[0]?.webSocketDebuggerUrl === 'string') { + return response[0]?.webSocketDebuggerUrl as string; + } + } catch { } + }, + () => { throw new Error(`Resolving websocket debugger url timed out ('${url}')`); } + ); +} + +function defineEvalScript(fn: () => unknown) { + const url = `eval-${randomBytes(4).toString('hex')}.cdp`; + const expression = `(${fn})();\n//# sourceURL=${url}\n`; + return Object.freeze({ expression, url }); +} + +async function retryableOperation( + callback: () => T | undefined | Promise, + onTimeout: () => T | never, +): Promise { + const timeoutExpiry = Date.now() + TIMEOUT_IN_MILLISECONDS; + do { + await new Promise(resolve => setTimeout(resolve, 50)); + const result = await callback(); + if (result !== undefined) { + return result; + } + } while (Date.now() < timeoutExpiry); + return onTimeout(); +} + +async function findLineNumber(sourceFile: string, line: string): Promise { + + const fileContent = await readFile(path.join(`${__dirname}/..`, sourceFile), 'utf8'); + + const lineIndex = fileContent.split('\n').findIndex(l => l.includes(line)); + if (lineIndex < 0) { + throw new Error(`Source line not found ('${line}' in '${sourceFile}')`); + } + + return lineIndex + 1; +} + +function sha256(bytes: BinaryLike) { + return createHash('sha256').update(bytes).digest('hex'); +} + +type RPCParamsType = ProtocolMapping.Commands[T]['paramsType'][0]; +type RPCReturnType = ProtocolMapping.Commands[T]['returnType']; + +class DebuggerRPC extends EventEmitter { + private connected = new Waitable(); + private socket: WebSocket; + private nextRequestId = 1; + private pendingRequests = new Map>(); + + constructor(url: string) { + super({ captureRejections: true }); + this.socket = new WebSocket(url, {}); + this.socket.binaryType = 'nodebuffer'; + this.socket.onopen = () => { + this.connected.resolve(); + }; + this.socket.onerror = e => { + this.connected.reject(e.error); + }; + this.socket.onmessage = async e => { + const data = JSON.parse((e.data as Buffer).toString('utf8')); + const { method, id, params, result, error } = data; + if (method) { + this.emit(method, params); + } else { + const responseReceived = this.pendingRequests.get(id); + if (responseReceived) { + this.pendingRequests.delete(id); + if (error) { + responseReceived.reject(new Error(error.message)); + } + else { + responseReceived.resolve(result); + } + } else { + console.warn(`[CdpDebuggerSession] Unrelated message received:`, data); + } + } + }; + } + + async send(method: T, params: RPCParamsType): Promise> { + await this.connected.wait(); + const id = this.nextRequestId++; + const responseReceived = new Waitable(); + this.socket.send(JSON.stringify({ id, method, params })); + this.pendingRequests.set(id, responseReceived); + return await responseReceived.wait() as RPCReturnType; + } + + dispose() { + const closed = new Waitable(); + this.socket.once('close', () => closed.resolve()); + this.socket.close(); + return closed.wait(); + } +} + +const WAIT_TIMED_OUT = Symbol(); + +class Waitable { + private callbacks: Parameters>[0]> | null; + private promise = new Promise((...args) => this.callbacks = args); + + get done() { + return this.callbacks === null; + } + + resolve(value: T) { + if (this.callbacks) { + this.callbacks[0](value); + this.callbacks = null; + } + } + + reject(reason: unknown) { + if (this.callbacks) { + this.callbacks[1](reason); + this.callbacks = null; + } + } + + wait() { + return Waitable.waitForResultOrTimeout(this.promise); + } + + reset() { + this.promise = new Promise((...args) => this.callbacks = args); + } + + static async waitForResultOrTimeout(promise: Promise) { + let timeoutId: NodeJS.Timeout = undefined!; + const result = await Promise.race([ + promise, + new Promise(resolve => timeoutId = setTimeout(() => resolve(WAIT_TIMED_OUT), TIMEOUT_IN_MILLISECONDS)) + ]); + if (result === WAIT_TIMED_OUT) { + throw new Error(`Waitable.wait() timed out after ${TIMEOUT_IN_SECONDS} seconds.`); + } + clearTimeout(timeoutId); + return result; + } +} diff --git a/test-utils/LLDBEvalDebugger.ts b/test-utils/LLDBEvalDebugger.ts new file mode 100644 index 0000000..aa638ae --- /dev/null +++ b/test-utils/LLDBEvalDebugger.ts @@ -0,0 +1,77 @@ +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Interpreter_test.ts + +import { Chrome } from '../src/ExtensionAPI'; +import type { Debugger, EvalResult } from '../wasm/symbols-backend/tests/LLDBEvalTests'; +import DebuggerSession from './DebuggerSession'; +import { createModuleRunner, DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD } from './emscripten-module-runner'; +import { SyncInterface } from './sync-interface'; + +const TEST_BINARY_FILE = 'wasm/symbols-backend/third_party/lldb-eval/src/testdata/test_binary.cc'; +const DEBUGGER_PORT = 9230; + +export class LLDBEvalDebugger implements Debugger { + debuggerSession: SyncInterface; + module = createModuleRunner('tests/inputs/lldb_eval_inputs.js', { debuggerPort: DEBUGGER_PORT }); + + runToLine(line: string): void { + this.module.run(); + this.debuggerSession = DebuggerSession.attachSync( + DEBUGGER_PORT, + `${DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD}/tests/inputs/lldb_eval_inputs.wasm`, + `${DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD}/tests/inputs/lldb_eval_inputs.wasm.debug.wasm`, + ); + + this.debuggerSession.continueToLine(TEST_BINARY_FILE, line); + this.debuggerSession.waitForPaused(); + } + + evaluate(expr: string): EvalResult { + try { + const resultObject = this.debuggerSession.evaluate(expr); + if (!resultObject) { + return { error: `Could not evaluate expression '${expr}'` }; + } + const result = this.#stringify(resultObject); + return { result }; + } catch (e) { + return { error: `${e}` }; + } + } + + exit(): void { + this.debuggerSession.dispose(); + } + + #stringify(result: Chrome.DevTools.RemoteObject | Chrome.DevTools.ForeignObject): string { + if (result.type === 'reftype') { + return 'reftype'; + } + if (result.objectId) { + const properties = this.debuggerSession.getProperties(result.objectId); + if (properties.length === 1) { + const [{ name }] = properties; + if (name.startsWith('0x')) { + return `0x${name.substring(2).padStart(8, '0')}`; + } + } + } + if (result.description === 'std::nullptr_t') { + return '0x00000000'; + } + if (Object.is(result.value, -0)) { + return '-0'; + } + if (result.value === -Infinity) { + return '-Inf'; + } + if (result.value === Infinity) { + return '+Inf'; + } + + return result.description ?? `${result.value}`; + } +} + diff --git a/test-utils/TestValue.ts b/test-utils/TestValue.ts index 9964d7a..d09d087 100644 --- a/test-utils/TestValue.ts +++ b/test-utils/TestValue.ts @@ -3,116 +3,11 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/TestUtils.ts -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; -import type {Value, WasmInterface} from '../src/CustomFormatters.js'; -import {WorkerPlugin} from '../src/DevToolsPluginHost.js'; -import type {WasmValue} from '../src/WasmTypes.js'; -import type {HostInterface} from '../src/WorkerRPC.js'; +import type { Value } from '../src/CustomFormatters'; -import type {Debugger} from './RealBackend.js'; - -export class TestHostInterface implements HostInterface { - getWasmLinearMemory(_offset: number, _length: number, _stopId: unknown): ArrayBuffer { - throw new Error('Method not implemented.'); - } - getWasmLocal(_local: number, _stopId: unknown): WasmValue { - throw new Error('Method not implemented.'); - } - getWasmGlobal(_global: number, _stopId: unknown): WasmValue { - throw new Error('Method not implemented.'); - } - getWasmOp(_op: number, _stopId: unknown): WasmValue { - throw new Error('Method not implemented.'); - } - reportResourceLoad( - _resourceUrl: string, - _status: {success: boolean, errorMessage?: string|undefined, size?: number|undefined}): Promise { - return Promise.resolve(); - } -} - -export function makeURL(path: string): string { - return new URL(path, document.baseURI).href; -} - -export async function createWorkerPlugin(debug?: Debugger): Promise { - return await WorkerPlugin.create([], true).then(p => { - if (debug) { - p.getWasmLinearMemory = debug.getWasmLinearMemory.bind(debug); - p.getWasmLocal = debug.getWasmLocal.bind(debug); - p.getWasmGlobal = debug.getWasmGlobal.bind(debug); - p.getWasmOp = debug.getWasmOp.bind(debug); - } - /* eslint-disable-next-line no-debugger */ - debugger; // Halt in the debugger to let developers set breakpoints in C++. - return p; - }); -} - -export function relativePathname(url: URL, base: URL): string { - const baseSplit = base.pathname.split('/'); - const urlSplit = url.pathname.split('/'); - - let i = 0; - for (; i < Math.min(baseSplit.length, urlSplit.length); ++i) { - if (baseSplit[i] !== urlSplit[i]) { - break; - } - } - const result = new Array(baseSplit.length - i); - result.fill('..'); - result.push(...urlSplit.slice(i).filter(p => p.length > 0)); - - return result.join('/'); -} - -export function nonNull(value: T|null|undefined): T { - assert.exists(value); - return value as T; -} - -export function remoteObject(value: Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject|null): - Chrome.DevTools.RemoteObject { - assert.exists(value); - assert(value.type !== 'reftype'); - return value; -} - -export class TestWasmInterface implements WasmInterface { - memory = new ArrayBuffer(0); - locals = new Map(); - globals = new Map(); - stack = new Map(); - - readMemory(offset: number, length: number): Uint8Array { - return new Uint8Array(this.memory, offset, length); - } - getOp(op: number): WasmValue { - const val = this.stack.get(op); - if (val !== undefined) { - return val; - } - throw new Error(`No stack entry ${op}`); - } - getLocal(local: number): WasmValue { - const val = this.locals.get(local); - if (val !== undefined) { - return val; - } - throw new Error(`No local ${local}`); - } - getGlobal(global: number): WasmValue { - const val = this.globals.get(global); - if (val !== undefined) { - return val; - } - throw new Error(`No global ${global}`); - } -} - -export class TestValue implements Value { +export default class TestValue implements Value { private dataView: DataView; - members: {[key: string]: TestValue, [key: number]: TestValue}; + members: { [key: string]: TestValue, [key: number]: TestValue; }; location: number; size: number; typeNames: string[]; @@ -167,20 +62,20 @@ export class TestValue implements Value { content.setFloat64(0, value, true); return new TestValue(content, typeName); } - static pointerTo(pointeeOrElements: TestValue|TestValue[], address?: number): TestValue { + static pointerTo(pointeeOrElements: TestValue | TestValue[], address?: number): TestValue { const content = new DataView(new ArrayBuffer(4)); const elements = Array.isArray(pointeeOrElements) ? pointeeOrElements : [pointeeOrElements]; address = address ?? elements[0].location; content.setUint32(0, address, true); const space = elements[0].typeNames[0].endsWith('*') ? '' : ' '; - const members: {[key: string|number]: TestValue} = {'*': elements[0]}; + const members: { [key: string | number]: TestValue; } = { '*': elements[0] }; for (let i = 0; i < elements.length; ++i) { members[i] = elements[i]; } const value = new TestValue(content, `${elements[0].typeNames[0]}${space}*`, members); return value; } - static fromMembers(typeName: string, members: {[key: string]: TestValue, [key: number]: TestValue}): TestValue { + static fromMembers(typeName: string, members: { [key: string]: TestValue, [key: number]: TestValue; }): TestValue { return new TestValue(new DataView(new ArrayBuffer(0)), typeName, members); } @@ -223,7 +118,7 @@ export class TestValue implements Value { return Object.keys(this.members); } - $(member: string|number): Value { + $(member: string | number): Value { if (typeof member === 'number' || !member.includes('.')) { return this.members[member]; } @@ -235,8 +130,8 @@ export class TestValue implements Value { } constructor( - content: DataView, typeName: string, - members?: {[key: string]: TestValue, [key: number]: TestValue}) { + content: DataView, typeName: string, + members?: { [key: string]: TestValue, [key: number]: TestValue; }) { this.location = 0; this.size = content.byteLength; this.typeNames = [typeName]; @@ -244,8 +139,3 @@ export class TestValue implements Value { this.dataView = content; } } - -declare global { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - let __karma__: unknown; -} diff --git a/test-utils/TestWasmInterface.ts b/test-utils/TestWasmInterface.ts new file mode 100644 index 0000000..64d66b6 --- /dev/null +++ b/test-utils/TestWasmInterface.ts @@ -0,0 +1,39 @@ +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/TestUtils.ts + +import type { WasmInterface } from '../src/CustomFormatters'; +import type { WasmValue } from '../src/WasmTypes'; + +export default class TestWasmInterface implements WasmInterface { + memory = new ArrayBuffer(0); + locals = new Map(); + globals = new Map(); + stack = new Map(); + + readMemory(offset: number, length: number): Uint8Array { + return new Uint8Array(this.memory, offset, length); + } + getOp(op: number): WasmValue { + const val = this.stack.get(op); + if (val !== undefined) { + return val; + } + throw new Error(`No stack entry ${op}`); + } + getLocal(local: number): WasmValue { + const val = this.locals.get(local); + if (val !== undefined) { + return val; + } + throw new Error(`No local ${local}`); + } + getGlobal(global: number): WasmValue { + const val = this.globals.get(global); + if (val !== undefined) { + return val; + } + throw new Error(`No global ${global}`); + } +} diff --git a/test-utils/emscripten-module-runner/child-process.js b/test-utils/emscripten-module-runner/child-process.js new file mode 100644 index 0000000..2f42f8e --- /dev/null +++ b/test-utils/emscripten-module-runner/child-process.js @@ -0,0 +1,8 @@ +const [moduleImportPath, callbackString] = process.argv.splice(2, 4); +const moduleImports = require(moduleImportPath); +const callback = eval(callbackString); + +// See: https://github.com/emscripten-core/emscripten/issues/16742 +global['Browser'] = { handledByPreloadPlugin: () => false }; + +callback(moduleImports); \ No newline at end of file diff --git a/test-utils/emscripten-module-runner/index.ts b/test-utils/emscripten-module-runner/index.ts new file mode 100644 index 0000000..d223c79 --- /dev/null +++ b/test-utils/emscripten-module-runner/index.ts @@ -0,0 +1,48 @@ +import { spawn, StdioOptions } from 'child_process'; +import * as path from 'path'; + +export interface EmscriptenModuleRunnerOptions { + cwd?: string; + stdio?: 'inherit' | 'ignore'; + imports?: string[]; + debuggerPort?: number; +} + +export interface EmscriptenModuleRunner { + run(callback?: (moduleFactory: EmscriptenModuleFactory) => void): Promise; +} + +export const DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD = `${__dirname}/../../wasm/symbols-backend.build/stage-2`; + +export function createModuleRunner(modulePath: string, options: EmscriptenModuleRunnerOptions = {}): EmscriptenModuleRunner { + const { cwd = DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD, stdio = 'ignore', imports = [], debuggerPort } = options; + return { + async run(callback = factory => factory()) { + const argv = [ + ...(debuggerPort ? [`--inspect-brk=${debuggerPort}`, '--inspect-publish-uid=http'] : []), + ...imports.flatMap(i => ['--import', i]), + `${__dirname}/child-process.js`, + path.resolve(cwd, modulePath), + callback.toString() + ]; + return spawnChildProcess(argv, stdio, cwd); + } + }; +} + +function spawnChildProcess(argv: string[], stdio: StdioOptions, cwd: string) { + return new Promise((resolve, reject) => spawn(process.execPath, argv, { stdio, cwd }) + .on('error', reject) + .on('exit', (code, signal) => { + if (signal) { + reject(new Error(`Child process exited because of signal ${signal}.`)); + } + else if (code !== 0) { + reject(new Error(`Child process exited with code ${code}.`)); + } + else { + resolve(); + } + }) + ); +} diff --git a/test-utils/sync-interface/index.ts b/test-utils/sync-interface/index.ts new file mode 100644 index 0000000..c443000 --- /dev/null +++ b/test-utils/sync-interface/index.ts @@ -0,0 +1,43 @@ +import assert from 'assert'; +import { MessageChannel, receiveMessageOnPort, Worker } from 'worker_threads'; + +const TIMEOUT_IN_MILLISECONDS = 30000; + +export type SyncInterface = { + [P in keyof T]: T[P] extends (...args: infer A) => Promise ? ((...args: A) => R) : T[P]; +}; + +export function createSyncInterface(module: string, method: string, ...args: any[]) { + const worker = new Worker(`${__dirname}/worker.js`); + const signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)); + const { port1: localPort, port2: remotePort } = new MessageChannel(); + + worker.postMessage({ module, method, args, port: remotePort, signalBuffer: signal.buffer }, [remotePort]); + waitForResponse(); + + return { + invoke(method: string, ...args: any[]): any { + localPort.postMessage({ method, args }); + return waitForResponse(); + }, + dispose(): void { + worker.unref(); + worker.postMessage({}); + } + }; + + function waitForResponse() { + const waitResult = Atomics.wait(signal, 0, 0, TIMEOUT_IN_MILLISECONDS); + assert(waitResult !== 'timed-out', `Synchronized worker command timed out`); + + const response = receiveMessageOnPort(localPort); + assert(response !== undefined, 'Synchronized worker command received no response'); + + const { result, error } = response.message; + if (error) { + throw error; + } + + return result; + } +} diff --git a/test-utils/sync-interface/worker.js b/test-utils/sync-interface/worker.js new file mode 100644 index 0000000..82a6526 --- /dev/null +++ b/test-utils/sync-interface/worker.js @@ -0,0 +1,49 @@ +import { parentPort } from 'worker_threads'; + +let signal, commandPort, instance; + +parentPort.once('message', init); + +async function init({ module, method, args, port, signalBuffer }) { + signal = new Int32Array(signalBuffer); + commandPort = port; + + try { + instance = await loadModule(module)[method](...args); + } catch (e) { + commandPort.postMessage({ error: e }); + Atomics.notify(signal, 0); + return; + } + + commandPort.postMessage({}); + Atomics.notify(signal, 0); + + commandPort.on('message', invoke); + parentPort.once('message', dispose); +} + +async function invoke({ method, args }) { + const response = {}; + try { + response.result = await instance[method](...args); + } catch (e) { + response.error = e; + } + + commandPort.postMessage(response); + Atomics.notify(signal, 0); +} + +function dispose() { + commandPort.close(); + instance = null; +} + +function loadModule(module) { + const moduleExport = require(module); + if (typeof moduleExport === 'function') { + return moduleExport; + } + return moduleExport.default; +} diff --git a/test/CustomFormatters.test.ts b/test/CustomFormatters.test.ts index d57be6f..33a926d 100644 --- a/test/CustomFormatters.test.ts +++ b/test/CustomFormatters.test.ts @@ -1,9 +1,12 @@ // This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 // of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. // -// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/CustomFormatters_test.ts +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Formatters_test.ts -import {MemorySlice, PageStore} from '../src/CustomFormatters.js'; +import assert from 'assert'; +import { describe, it } from 'node:test'; + +import { MemorySlice, PageStore } from '../src/CustomFormatters'; function asArray(slice: MemorySlice): number[] { return Array.from(new Uint8Array(slice.buffer)); @@ -14,11 +17,15 @@ describe('PageStore', () => { const bufferA = new Uint8Array([1, 2, 3, 4]).buffer; const bufferB = new Uint8Array([5, 6, 7, 8]).buffer; - expect(() => new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 32))) - .to.throw('Slices are not contiguous'); - expect(() => new MemorySlice(bufferA, 32).merge(new MemorySlice(bufferB, 16))) - .to.throw('Slices are not contiguous'); - expect(asArray(new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 20)))).to.deep.equal([ + assert.throws( + () => new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 32)), + new Error('Slices are not contiguous') + ); + assert.throws( + () => new MemorySlice(bufferA, 32).merge(new MemorySlice(bufferB, 16)), + new Error('Slices are not contiguous') + ); + assert.deepEqual(asArray(new MemorySlice(bufferA, 16).merge(new MemorySlice(bufferB, 20))), [ 1, 2, 3, @@ -28,7 +35,7 @@ describe('PageStore', () => { 7, 8, ]); - expect(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 16)))).to.deep.equal([ + assert.deepEqual(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 16))), [ 1, 2, 3, @@ -38,7 +45,7 @@ describe('PageStore', () => { 7, 8, ]); - expect(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 16)))).to.deep.equal([ + assert.deepEqual(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 16))), [ 5, 6, 7, @@ -48,7 +55,7 @@ describe('PageStore', () => { 3, 4, ]); - expect(asArray(new MemorySlice(bufferB, 16).merge(new MemorySlice(bufferA, 20)))).to.deep.equal([ + assert.deepEqual(asArray(new MemorySlice(bufferB, 16).merge(new MemorySlice(bufferA, 20))), [ 5, 6, 7, @@ -58,10 +65,10 @@ describe('PageStore', () => { 3, 4, ]); - expect(asArray(new MemorySlice(bufferA, 18).merge(new MemorySlice(bufferB, 20)))).to.deep.equal([1, 2, 3, 4, 7, 8]); - expect(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 18)))).to.deep.equal([1, 2, 3, 4, 7, 8]); - expect(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 18)))).to.deep.equal([5, 6, 7, 8, 3, 4]); - expect(asArray(new MemorySlice(bufferB, 18).merge(new MemorySlice(bufferA, 20)))).to.deep.equal([5, 6, 7, 8, 3, 4]); + assert.deepEqual(asArray(new MemorySlice(bufferA, 18).merge(new MemorySlice(bufferB, 20))), [1, 2, 3, 4, 7, 8]); + assert.deepEqual(asArray(new MemorySlice(bufferB, 20).merge(new MemorySlice(bufferA, 18))), [1, 2, 3, 4, 7, 8]); + assert.deepEqual(asArray(new MemorySlice(bufferA, 20).merge(new MemorySlice(bufferB, 18))), [5, 6, 7, 8, 3, 4]); + assert.deepEqual(asArray(new MemorySlice(bufferB, 18).merge(new MemorySlice(bufferA, 20))), [5, 6, 7, 8, 3, 4]); }); it('sorts disjoint slices correctly', () => { @@ -74,7 +81,7 @@ describe('PageStore', () => { view.addSlice(two, 8); view.addSlice(two, 11); view.addSlice(two, 14); - expect(view.slices.map(s => s.begin)).to.deep.equal([2, 5, 8, 11, 14]); + assert.deepEqual(view.slices.map(s => s.begin), [2, 5, 8, 11, 14]); } { const view = new PageStore(); @@ -83,27 +90,27 @@ describe('PageStore', () => { view.addSlice(two, 8); view.addSlice(two, 5); view.addSlice(two, 2); - expect(view.slices.map(s => s.begin)).to.deep.equal([2, 5, 8, 11, 14]); + assert.deepEqual(view.slices.map(s => s.begin), [2, 5, 8, 11, 14]); } }); it('finds slice indices correctly', () => { const four = new Uint8Array([3, 4, 5, 6]).buffer; const view = new PageStore(); - expect(view.findSliceIndex(100)).to.eql(-1); + assert.equal(view.findSliceIndex(100), -1); for (const offset of [2, 7, 12, 17, 22]) { view.addSlice(four, offset); - expect(view.findSliceIndex(0)).to.eql(-1); - expect(view.findSliceIndex(100)).to.eql(view.slices.length - 1); + assert.equal(view.findSliceIndex(0), -1); + assert.equal(view.findSliceIndex(100), view.slices.length - 1); for (let s = 0; s < view.slices.length; ++s) { const slice = view.slices[s]; - expect(view.findSliceIndex(slice.begin - 1)).to.eql(s - 1); - expect(view.findSliceIndex(slice.begin)).to.eql(s); - expect(view.findSliceIndex(slice.begin + 1)).to.eql(s); - expect(view.findSliceIndex(slice.end - 1)).to.eql(s); - expect(view.findSliceIndex(slice.end)).to.eql(s); + assert.equal(view.findSliceIndex(slice.begin - 1), s - 1); + assert.equal(view.findSliceIndex(slice.begin), s); + assert.equal(view.findSliceIndex(slice.begin + 1), s); + assert.equal(view.findSliceIndex(slice.end - 1), s); + assert.equal(view.findSliceIndex(slice.end), s); } } }); @@ -111,17 +118,17 @@ describe('PageStore', () => { it('finds offsets correctly', () => { const four = new Uint8Array([3, 4, 5, 6]).buffer; const view = new PageStore(); - expect(view.findSlice(100)).to.eql(null); + assert.equal(view.findSlice(100), null); for (const offset of [2, 7, 12, 17, 22]) { view.addSlice(four, offset); - expect(view.findSlice(100)).to.eql(null); + assert.equal(view.findSlice(100), null); for (const slice of view.slices) { - expect(view.findSlice(slice.begin - 1)).to.eql(null); - expect(view.findSlice(slice.begin)).to.eql(slice); - expect(view.findSlice(slice.begin + 1)).to.eql(slice); - expect(view.findSlice(slice.end - 1)).to.eql(slice); - expect(view.findSlice(slice.end)).to.eql(null); + assert.equal(view.findSlice(slice.begin - 1), null); + assert.equal(view.findSlice(slice.begin), slice); + assert.equal(view.findSlice(slice.begin + 1), slice); + assert.equal(view.findSlice(slice.end - 1), slice); + assert.equal(view.findSlice(slice.end), null); } } }); @@ -131,12 +138,12 @@ describe('PageStore', () => { const view = new PageStore(); view.addSlice([1, 2], 2); view.addSlice([2, 3], 3); - expect(view.slices.length).to.eql(1); - expect(asArray(view.slices[0])).to.deep.equal([1, 2, 3]); + assert.equal(view.slices.length, 1); + assert.deepEqual(asArray(view.slices[0]), [1, 2, 3]); view.addSlice([0, 1, 2, 3, 4], 1); - expect(view.slices.length).to.eql(1); - expect(asArray(view.slices[0])).to.deep.equal([0, 1, 2, 3, 4]); + assert.equal(view.slices.length, 1); + assert.deepEqual(asArray(view.slices[0]), [0, 1, 2, 3, 4]); } const getView = (): PageStore => { @@ -154,15 +161,15 @@ describe('PageStore', () => { // --XX--XX--XX--XX // || view.addSlice([5, 4], 5); - expect(view.slices.length).to.eql(4); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([5, 4, 5]); - expect(view.slices[1].begin).to.eql(5); - expect(asArray(view.slices[2])).to.deep.equal([7, 8]); - expect(view.slices[2].begin).to.eql(10); - expect(asArray(view.slices[3])).to.deep.equal([10, 11]); - expect(view.slices[3].begin).to.eql(14); + assert.equal(view.slices.length, 4); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [5, 4, 5]); + assert.equal(view.slices[1].begin, 5); + assert.deepEqual(asArray(view.slices[2]), [7, 8]); + assert.equal(view.slices[2].begin, 10); + assert.deepEqual(asArray(view.slices[3]), [10, 11]); + assert.equal(view.slices[3].begin, 14); } { @@ -170,15 +177,15 @@ describe('PageStore', () => { // --XX--XX--XX--XX // || view.addSlice([4, 5], 6); - expect(view.slices.length).to.eql(4); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([7, 8]); - expect(view.slices[2].begin).to.eql(10); - expect(asArray(view.slices[3])).to.deep.equal([10, 11]); - expect(view.slices[3].begin).to.eql(14); + assert.equal(view.slices.length, 4); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [7, 8]); + assert.equal(view.slices[2].begin, 10); + assert.deepEqual(asArray(view.slices[3]), [10, 11]); + assert.equal(view.slices[3].begin, 14); } { @@ -186,15 +193,15 @@ describe('PageStore', () => { // --XX--XX--XX--XX // || view.addSlice([5, 6], 7); - expect(view.slices.length).to.eql(4); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5, 6]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([7, 8]); - expect(view.slices[2].begin).to.eql(10); - expect(asArray(view.slices[3])).to.deep.equal([10, 11]); - expect(view.slices[3].begin).to.eql(14); + assert.equal(view.slices.length, 4); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5, 6]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [7, 8]); + assert.equal(view.slices[2].begin, 10); + assert.deepEqual(asArray(view.slices[3]), [10, 11]); + assert.equal(view.slices[3].begin, 14); } { @@ -203,20 +210,20 @@ describe('PageStore', () => { // --XX--XX--XX--XX----XX // || view.addSlice([17, 18], 17); - expect(view.slices.length).to.eql(6); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([7, 8]); - expect(view.slices[2].begin).to.eql(10); - expect(asArray(view.slices[3])).to.deep.equal([10, 11]); - expect(view.slices[3].begin).to.eql(14); - - expect(asArray(view.slices[4])).to.deep.equal([17, 18]); - expect(view.slices[4].begin).to.eql(17); - expect(asArray(view.slices[5])).to.deep.equal([20, 21]); - expect(view.slices[5].begin).to.eql(20); + assert.equal(view.slices.length, 6); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [7, 8]); + assert.equal(view.slices[2].begin, 10); + assert.deepEqual(asArray(view.slices[3]), [10, 11]); + assert.equal(view.slices[3].begin, 14); + + assert.deepEqual(asArray(view.slices[4]), [17, 18]); + assert.equal(view.slices[4].begin, 17); + assert.deepEqual(asArray(view.slices[5]), [20, 21]); + assert.equal(view.slices[5].begin, 20); } { @@ -225,11 +232,11 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |______| view.addSlice([0, 0, 4, 5, 0, 0, 7, 8], 4); - expect(view.slices.length).to.eql(2); - expect(asArray(view.slices[0])).to.deep.equal([1, 2, 0, 0, 4, 5, 0, 0, 7, 8]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([10, 11]); - expect(view.slices[1].begin).to.eql(14); + assert.equal(view.slices.length, 2); + assert.deepEqual(asArray(view.slices[0]), [1, 2, 0, 0, 4, 5, 0, 0, 7, 8]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [10, 11]); + assert.equal(view.slices[1].begin, 14); } { @@ -238,13 +245,13 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |_____| view.addSlice([0, 4, 5, 0, 0, 7, 8], 5); - expect(view.slices.length).to.eql(3); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([0, 4, 5, 0, 0, 7, 8]); - expect(view.slices[1].begin).to.eql(5); - expect(asArray(view.slices[2])).to.deep.equal([10, 11]); - expect(view.slices[2].begin).to.eql(14); + assert.equal(view.slices.length, 3); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [0, 4, 5, 0, 0, 7, 8]); + assert.equal(view.slices[1].begin, 5); + assert.deepEqual(asArray(view.slices[2]), [10, 11]); + assert.equal(view.slices[2].begin, 14); } { @@ -253,13 +260,13 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |___| view.addSlice([5, 0, 0, 7, 8], 7); - expect(view.slices.length).to.eql(3); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([10, 11]); - expect(view.slices[2].begin).to.eql(14); + assert.equal(view.slices.length, 3); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5, 0, 0, 7, 8]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [10, 11]); + assert.equal(view.slices[2].begin, 14); } { @@ -268,13 +275,13 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |____| view.addSlice([4, 5, 0, 0, 7, 8], 6); - expect(view.slices.length).to.eql(3); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([10, 11]); - expect(view.slices[2].begin).to.eql(14); + assert.equal(view.slices.length, 3); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5, 0, 0, 7, 8]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [10, 11]); + assert.equal(view.slices[2].begin, 14); } { @@ -283,13 +290,13 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |_____| view.addSlice([4, 5, 0, 0, 7, 8, 0], 6); - expect(view.slices.length).to.eql(3); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8, 0]); - expect(view.slices[1].begin).to.eql(6); - expect(asArray(view.slices[2])).to.deep.equal([10, 11]); - expect(view.slices[2].begin).to.eql(14); + assert.equal(view.slices.length, 3); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5, 0, 0, 7, 8, 0]); + assert.equal(view.slices[1].begin, 6); + assert.deepEqual(asArray(view.slices[2]), [10, 11]); + assert.equal(view.slices[2].begin, 14); } { @@ -298,11 +305,11 @@ describe('PageStore', () => { // --XX--XX--XX--XX // |______| view.addSlice([4, 5, 0, 0, 7, 8, 0, 0], 6); - expect(view.slices.length).to.eql(2); - expect(asArray(view.slices[0])).to.deep.equal([1, 2]); - expect(view.slices[0].begin).to.eql(2); - expect(asArray(view.slices[1])).to.deep.equal([4, 5, 0, 0, 7, 8, 0, 0, 10, 11]); - expect(view.slices[1].begin).to.eql(6); + assert.equal(view.slices.length, 2); + assert.deepEqual(asArray(view.slices[0]), [1, 2]); + assert.equal(view.slices[0].begin, 2); + assert.deepEqual(asArray(view.slices[1]), [4, 5, 0, 0, 7, 8, 0, 0, 10, 11]); + assert.equal(view.slices[1].begin, 6); } }); }); diff --git a/test/Formatters.test.ts b/test/Formatters.test.ts index d6e8037..6d516bc 100644 --- a/test/Formatters.test.ts +++ b/test/Formatters.test.ts @@ -3,10 +3,13 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Formatters_test.ts -import {CustomFormatters, type TypeInfo} from '../src/CustomFormatters.js'; -import * as Formatters from '../src/Formatters.js'; +import assert from 'assert'; +import { describe, it } from 'node:test'; -import {TestValue, TestWasmInterface} from './TestUtils.js'; +import { CustomFormatters, type TypeInfo } from '../src/CustomFormatters'; +import * as Formatters from '../src/Formatters'; +import TestValue from '../test-utils/TestValue'; +import TestWasmInterface from '../test-utils/TestWasmInterface'; describe('Formatters', () => { it('formatChar', () => { @@ -39,106 +42,148 @@ describe('Formatters', () => { // 0x0 const ptr = TestValue.fromUint32(0, 'char *'); - assert.deepEqual(Formatters.formatCString(wasm, ptr), {'0x0': null}); - assert.deepEqual(Formatters.formatU16CString(wasm, ptr), {'0x0': null}); - assert.deepEqual(Formatters.formatCWString(wasm, ptr), {'0x0': null}); + assert.deepEqual(Formatters.formatCString(wasm, ptr), { '0x0': null }); + assert.deepEqual(Formatters.formatU16CString(wasm, ptr), { '0x0': null }); + assert.deepEqual(Formatters.formatCWString(wasm, ptr), { '0x0': null }); // short string - const shortString = 'abcdef\0'; + const shortString = 'abcdef'; + const shortStringQuoted = `"${shortString}"`; + const shortStringChars = new TextEncoder().encode(shortString + '\0'); const shortStringValue = - new TestValue(new DataView(new TextEncoder().encode(shortString).buffer as ArrayBuffer), 'char'); + new TestValue(new DataView(shortStringChars.buffer), 'char'); assert.deepEqual( - Formatters.formatCString(wasm, TestValue.pointerTo(shortStringValue, Formatters.Constants.SAFE_HEAP_START)), - 'abcdef'); - - const shortContents = new DataView(new ArrayBuffer(shortString.length * Uint32Array.BYTES_PER_ELEMENT)); - for (let i = 0; i < shortString.length; ++i) { - shortContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, shortString.codePointAt(i) ?? 0, true); + Formatters.formatCString(wasm, TestValue.pointerTo(shortStringValue, Formatters.Constants.SAFE_HEAP_START)), + { + size: shortStringChars.length - 1, + string: shortStringQuoted, + chars: shortStringChars.slice(0, shortStringChars.length - 1), + }); + + const wideStringChars = new Uint32Array(shortString.length + 1); + const wideStringContents = new DataView(wideStringChars.buffer); + for (let i = 0; i < wideStringChars.length; ++i) { + wideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, shortString.codePointAt(i) ?? 0, true); } - const shortWString = new TestValue(shortContents, 'wchar_t'); + const shortWString = new TestValue(wideStringContents, 'wchar_t'); assert.deepEqual( - Formatters.formatCWString(wasm, TestValue.pointerTo(shortWString, Formatters.Constants.SAFE_HEAP_START)), - 'abcdef'); + Formatters.formatCWString(wasm, TestValue.pointerTo(shortWString, Formatters.Constants.SAFE_HEAP_START)), + { + size: wideStringChars.length - 1, + string: shortStringQuoted, + chars: wideStringChars.slice(0, wideStringChars.length - 1), + }); // long string - const longString = `${new Array(Formatters.Constants.PAGE_SIZE / 4).fill('abcdefg').join('')}\0`; - const longStringValue = - new TestValue(new DataView(new TextEncoder().encode(longString).buffer as ArrayBuffer), 'char'); + const longString = `${new Array(Formatters.Constants.PAGE_SIZE / 4).fill('abcdefg').join('')}`; + const longStringQuoted = `"${longString}"`; + const longStringChars = new TextEncoder().encode(longString + '\0'); + const longStringValue = new TestValue(new DataView(longStringChars.buffer), 'char'); assert.deepEqual( - Formatters.formatCString(wasm, TestValue.pointerTo(longStringValue, Formatters.Constants.SAFE_HEAP_START)), - longString.substr(0, longString.length - 1)); - - const longContents = new DataView(new ArrayBuffer(longString.length * Uint32Array.BYTES_PER_ELEMENT)); - for (let i = 0; i < longString.length; ++i) { - longContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, longString.codePointAt(i) ?? 0, true); + Formatters.formatCString(wasm, TestValue.pointerTo(longStringValue, Formatters.Constants.SAFE_HEAP_START)), + { + size: longStringChars.length - 1, + string: longStringQuoted, + chars: longStringChars.slice(0, longStringChars.length - 1), + }); + + const longWideStringChars = new Uint32Array(longString.length + 1); + const longWideStringContents = new DataView(longWideStringChars.buffer); + for (let i = 0; i < longWideStringChars.length; ++i) { + longWideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, longString.codePointAt(i) ?? 0, true); } - const longWString = new TestValue(longContents, 'wchar_t'); + const longWString = new TestValue(longWideStringContents, 'wchar_t'); assert.deepEqual( - Formatters.formatCWString(wasm, TestValue.pointerTo(longWString, Formatters.Constants.SAFE_HEAP_START)), - longString.substr(0, longString.length - 1)); + Formatters.formatCWString(wasm, TestValue.pointerTo(longWString, Formatters.Constants.SAFE_HEAP_START)), + { + size: longWideStringChars.length - 1, + string: longStringQuoted, + chars: longWideStringChars.slice(0, longWideStringChars.length - 1), + }); }); it('formatLibCXXString', () => { const wasm = new TestWasmInterface(); // short string const shortString = 'abcdefgh'; + const shortStringQuoted = `"${shortString}"`; + const shortStringChars = new TextEncoder().encode(shortString); const shortFlag = TestValue.fromUint8(shortString.length); const longString = new Array(128 / shortString.length).fill(shortString).join(''); + const longStringQuoted = `"${longString}"`; + const longStringChars = new TextEncoder().encode(longString); const longFlag = TestValue.fromUint8(0x80); // eslint-disable-next-line @typescript-eslint/naming-convention const __s_union = TestValue.fromMembers('__s_union', {}); // eslint-disable-next-line @typescript-eslint/naming-convention - const __s = TestValue.fromMembers('__s', {'': __s_union}); + const __s = TestValue.fromMembers('__s', { '': __s_union }); // eslint-disable-next-line @typescript-eslint/naming-convention const __l = TestValue.fromMembers('__l', {}); const str = TestValue.fromMembers('std::string', { __r_: TestValue.fromMembers('__r_', { - __value_: TestValue.fromMembers('__value_', {'': TestValue.fromMembers('__value_union', {__s, __l})}), + __value_: TestValue.fromMembers('__value_', { '': TestValue.fromMembers('__value_union', { __s, __l }) }), }), }); // short char8_t - __s.members.__data_ = - new TestValue(new DataView(new TextEncoder().encode(shortString).buffer as ArrayBuffer), 'char'); + __s.members.__data_ = new TestValue(new DataView(shortStringChars.buffer), 'char'); __s_union.members.__size_ = shortFlag; - assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), {size: shortString.length, string: shortString}); + assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), { + size: shortStringChars.length, + string: shortStringQuoted, + chars: shortStringChars, + }); // long char8_t - const wideStringContents = new DataView(new ArrayBuffer(shortString.length * Uint32Array.BYTES_PER_ELEMENT)); + const wideStringChars = new Uint32Array(shortString.length); + const wideStringContents = new DataView(wideStringChars.buffer); for (let i = 0; i < shortString.length; ++i) { wideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, shortString.codePointAt(i) ?? 0, true); } __s.members.__data_ = new TestValue(wideStringContents, 'wchar_t'); __s_union.members.__size_ = shortFlag; - assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), {size: shortString.length, string: shortString}); + assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), { + size: wideStringChars.length, + string: shortStringQuoted, + chars: wideStringChars, + }); // long char8_t wasm.memory = new ArrayBuffer(Formatters.Constants.SAFE_HEAP_START + longString.length); - new Uint8Array(wasm.memory).set(new TextEncoder().encode(longString), Formatters.Constants.SAFE_HEAP_START); + new Uint8Array(wasm.memory).set(longStringChars, Formatters.Constants.SAFE_HEAP_START); __l.members.__data_ = TestValue.pointerTo( - new TestValue(new DataView(wasm.memory, Formatters.Constants.SAFE_HEAP_START), 'char'), - Formatters.Constants.SAFE_HEAP_START); + new TestValue(new DataView(wasm.memory, Formatters.Constants.SAFE_HEAP_START), 'char'), + Formatters.Constants.SAFE_HEAP_START); __s_union.members.__size_ = longFlag; __l.members.__size_ = TestValue.fromUint32(longString.length); - assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), {size: longString.length, string: longString}); + assert.deepEqual(Formatters.formatLibCXX8String(wasm, str), { + size: longStringChars.length, + string: longStringQuoted, + chars: longStringChars, + }); // long char32_t wasm.memory = - new ArrayBuffer(Formatters.Constants.SAFE_HEAP_START + longString.length * Uint32Array.BYTES_PER_ELEMENT); + new ArrayBuffer(Formatters.Constants.SAFE_HEAP_START + longString.length * Uint32Array.BYTES_PER_ELEMENT); + const longWideStringChars = new Uint32Array(wasm.memory, Formatters.Constants.SAFE_HEAP_START); const longWideStringContents = new DataView(wasm.memory, Formatters.Constants.SAFE_HEAP_START); for (let i = 0; i < longString.length; ++i) { longWideStringContents.setUint32(i * Uint32Array.BYTES_PER_ELEMENT, longString.codePointAt(i) ?? 0, true); } __l.members.__data_ = - TestValue.pointerTo(new TestValue(longWideStringContents, 'wchar_t'), Formatters.Constants.SAFE_HEAP_START); + TestValue.pointerTo(new TestValue(longWideStringContents, 'wchar_t'), Formatters.Constants.SAFE_HEAP_START); __s_union.members.__size_ = longFlag; __l.members.__size_ = TestValue.fromUint32(longString.length); - assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), {size: longString.length, string: longString}); + assert.deepEqual(Formatters.formatLibCXX32String(wasm, str), { + size: longWideStringChars.length, + string: longStringQuoted, + chars: longWideStringChars, + }); }); it('formatVector', () => { @@ -146,28 +191,28 @@ describe('Formatters', () => { const elements = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(v => TestValue.fromFloat32(v)); const __begin_ = TestValue.pointerTo(elements, 0x1234); // eslint-disable-line @typescript-eslint/naming-convention const __end_ = // eslint-disable-line @typescript-eslint/naming-convention - TestValue.pointerTo(elements[elements.length - 1], 0x1234 + Float32Array.BYTES_PER_ELEMENT * elements.length); - const vector = new TestValue(new DataView(new ArrayBuffer(0)), 'std::vector', {__begin_, __end_}); + TestValue.pointerTo(elements[elements.length - 1], 0x1234 + Float32Array.BYTES_PER_ELEMENT * elements.length); + const vector = new TestValue(new DataView(new ArrayBuffer(0)), 'std::vector', { __begin_, __end_ }); assert.deepEqual(Formatters.formatVector(wasm, vector), elements); }); it('formatPointerOrReference', () => { const wasm = new TestWasmInterface(); - assert.deepEqual(Formatters.formatPointerOrReference(wasm, TestValue.fromUint32(0)), {'0x0': null}); + assert.deepEqual(Formatters.formatPointerOrReference(wasm, TestValue.fromUint32(0)), { '0x0': null }); const pointee = TestValue.fromFloat64(15); assert.deepEqual( - Formatters.formatPointerOrReference(wasm, TestValue.pointerTo(pointee, 0x1234)), {'0x1234': pointee}); + Formatters.formatPointerOrReference(wasm, TestValue.pointerTo(pointee, 0x1234)), { '0x1234': pointee }); }); it('formatDynamicArray', () => { const wasm = new TestWasmInterface(); const element = TestValue.fromFloat32(5); - const array = new TestValue(element.asDataView(), 'float[]', {0: element}); + const array = new TestValue(element.asDataView(), 'float[]', { 0: element }); array.location = 0x1234; - assert.deepEqual(Formatters.formatDynamicArray(wasm, array), {'0x1234': element}); + assert.deepEqual(Formatters.formatDynamicArray(wasm, array), { '0x1234': element }); }); it('formatUint128', () => { @@ -228,40 +273,40 @@ describe('CustomFormatters', () => { assert.deepEqual(CustomFormatters.get(type('std::__2::string'))?.format, Formatters.formatLibCXX8String); assert.deepEqual( - CustomFormatters - .get(type('std::__2::basic_string, std::__2::allocator >')) - ?.format, - Formatters.formatLibCXX8String); + CustomFormatters + .get(type('std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX8String); assert.deepEqual(CustomFormatters.get(type('std::__2::u8string'))?.format, Formatters.formatLibCXX8String); assert.deepEqual( - CustomFormatters - .get(type( - 'std::__2::basic_string, std::__2::allocator >')) - ?.format, - Formatters.formatLibCXX8String); + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX8String); assert.deepEqual(CustomFormatters.get(type('std::__2::u16string'))?.format, Formatters.formatLibCXX16String); assert.deepEqual( - CustomFormatters - .get(type( - 'std::__2::basic_string, std::__2::allocator >')) - ?.format, - Formatters.formatLibCXX16String); + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX16String); assert.deepEqual(CustomFormatters.get(type('std::__2::wstring'))?.format, Formatters.formatLibCXX32String); assert.deepEqual( - CustomFormatters - .get(type( - 'std::__2::basic_string, std::__2::allocator >')) - ?.format, - Formatters.formatLibCXX32String); + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX32String); assert.deepEqual(CustomFormatters.get(type('std::__2::u32string'))?.format, Formatters.formatLibCXX32String); assert.deepEqual( - CustomFormatters - .get(type( - 'std::__2::basic_string, std::__2::allocator >')) - ?.format, - Formatters.formatLibCXX32String); + CustomFormatters + .get(type( + 'std::__2::basic_string, std::__2::allocator >')) + ?.format, + Formatters.formatLibCXX32String); assert.deepEqual(CustomFormatters.get(type('char *'))?.format, Formatters.formatCString); assert.deepEqual(CustomFormatters.get(type('char8_t *'))?.format, Formatters.formatCString); @@ -269,7 +314,7 @@ describe('CustomFormatters', () => { assert.deepEqual(CustomFormatters.get(type('wchar_t *'))?.format, Formatters.formatCWString); assert.deepEqual(CustomFormatters.get(type('char32_t *'))?.format, Formatters.formatCWString); assert.deepEqual( - CustomFormatters.get(type('int (*)()', {isPointer: true}))?.format, Formatters.formatPointerOrReference); + CustomFormatters.get(type('int (*)()', { isPointer: true }))?.format, Formatters.formatPointerOrReference); assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); diff --git a/test/LLDBEval.test.ts b/test/LLDBEval.test.ts deleted file mode 100644 index 5f1d9f3..0000000 --- a/test/LLDBEval.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 -// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. -// -// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Interpreter_test.ts - -import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; -import {createEmbindPool} from '../src/DWARFSymbols.js'; - -import type * as LLDBEvalTests from './LLDBEvalTests.js'; -import loadModule from './LLDBEvalTests.js'; -import {Debugger} from './RealBackend.js'; -import {createWorkerPlugin, makeURL, remoteObject} from './TestUtils.js'; - -const WASM_URL = makeURL('/build/tests/inputs/lldb_eval_inputs.wasm'); -class LLDBEvalDebugger implements LLDBEvalTests.Debugger { - #debugger: Debugger; - #plugin: Chrome.DevTools.LanguageExtensionPlugin; - constructor(dbg: Debugger, plugin: Chrome.DevTools.LanguageExtensionPlugin) { - this.#debugger = dbg; - this.#plugin = plugin; - } - - static async create(): Promise { - const dbg = await Debugger.create(); - const plugin = await createWorkerPlugin(dbg); - return new LLDBEvalDebugger(dbg, plugin); - } - - private async stringify(result: Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject): Promise { - if (!this.#plugin.getProperties) { - throw new Error('getProperties not implemented'); - } - if (result.type === 'reftype') { - return 'reftype'; - } - if (result.objectId) { - const properties = await this.#plugin.getProperties(result.objectId); - if (properties.length === 1) { - const [{name}] = properties; - if (name.startsWith('0x')) { - return `0x${name.substring(2).padStart(8, '0')}`; - } - } - } - if (result.description === 'std::nullptr_t') { - return '0x00000000'; - } - if (Object.is(result.value, -0)) { - return '-0'; - } - if (result.value === -Infinity) { - return '-Inf'; - } - if (result.value === Infinity) { - return '+Inf'; - } - - return result.description ?? `${result.value}`; - } - - async evaluate(expr: string): Promise { - const {callFrame, rawLocation} = await this.#debugger.waitForPause(); - if (!this.#plugin.evaluate) { - throw new Error('Not implemented'); - } - try { - const resultObject = await this.#plugin.evaluate(expr, rawLocation, this.#debugger.stopIdForCallFrame(callFrame)); - if (!resultObject) { - return {error: `Could not evaluate expression '${expr}'`}; - } - const result = await this.stringify(resultObject); - return {result}; - - } catch (e) { - return {error: `${e}`}; - } - } - - async exit(): Promise { - if (this.#debugger.isPaused()) { - await this.#debugger.clearBreakpoints(); - await this.#debugger.resume(); - const rawModuleId = await this.#debugger.waitForScript(WASM_URL); - await this.#plugin.removeRawModule(rawModuleId); - } - } - - async runToLine(line: string): Promise { - const page = this.#debugger.page('./lldb_eval_inputs.js'); - await page.open(); - - const rawModuleId = await this.#debugger.waitForScript(WASM_URL); - - const url = makeURL('/build/tests/inputs/lldb_eval_inputs.wasm.debug.wasm'); - const sources = await this.#plugin.addRawModule(rawModuleId, '', {url}); - if ('missingSymbolFiles' in sources) { - throw new Error('Unexpected missing symbol files'); - } - const sourceFileURL = sources.find(s => s.endsWith('test_binary.cc')); - if (!sourceFileURL) { - throw new Error('test_binary.cc source not found'); - } - - const breakpoint = - await this.#debugger.setBreakpointOnSourceLine(line, new URL(sourceFileURL), this.#plugin, rawModuleId); - - const goPromise = page.go(); - const pauseOrExitcode = await Promise.race([goPromise, this.#debugger.waitForPause()]); - if (typeof pauseOrExitcode === 'number') { - throw new Error('Program terminated before all breakpoints were hit.'); - } - - const {rawLocation} = pauseOrExitcode; - const [sourceLocation] = await this.#plugin.rawLocationToSourceLocation(rawLocation); - if (sourceLocation?.lineNumber !== breakpoint.lineNumber) { - throw new Error( - `Paused on unexpected line ${sourceLocation?.lineNumber}. Breakpoint was set on ${breakpoint.lineNumber}.`); - } - } - - close(): Promise { - return this.#debugger.close(); - } -} - -describe('Interpreter', () => { - it('passes the lldb-eval test suite.', async () => { - const lldbEval = await loadModule(); - const debug = await LLDBEvalDebugger.create(); - - const {manage, flush} = createEmbindPool(); - try { - const argv = manage(new lldbEval.StringArray()); - - const skippedTests = [ - 'EvalTest.TestTemplateTypes', - 'EvalTest.TestUnscopedEnumNegation', - 'EvalTest.TestUniquePtrDeref', - 'EvalTest.TestUniquePtrCompare', - ]; - argv.push_back(`--gtest_filter=-${skippedTests.join(':')}`); - - const exitCode = await lldbEval.runTests(debug, argv); - assert.strictEqual(exitCode, 0, 'gtest test suite failed'); - } finally { - flush(); - } - }); - - it('can do basic arithmetic.', async () => { - const debug = await Debugger.create(); - const page = debug.page('./addresses_main.js'); - await page.open(); - - const wasmUrl = makeURL('/build/tests/inputs/addresses_main.wasm'); - const rawModuleId = await debug.waitForScript(wasmUrl); - const plugin = await createWorkerPlugin(debug); - - const url = makeURL('/build/tests/inputs/addresses_main.wasm.debug.wasm'); - const sources = await plugin.addRawModule(rawModuleId, '', {url}); - if ('missingSymbolFiles' in sources) { - throw new Error('Unexpected missing symbol files'); - } - const sourceFileURL = sources.find(s => s.endsWith('addresses.cc')); - if (!sourceFileURL) { - throw new Error('addresses.cc source not found'); - } - - const {lineNumber} = await debug.setBreakpointOnSourceLine( - '// BREAK(ArrayMembersTest)', new URL(sourceFileURL), plugin, rawModuleId); - - const goPromise = page.go(); - const pauseOrExitcode = await Promise.race([debug.waitForPause(), goPromise]); - if (typeof pauseOrExitcode === 'number') { - throw new Error('Program terminated before all breakpoints were hit.'); - } - - const {callFrame, rawLocation} = pauseOrExitcode; - - const [sourceLocation] = await plugin.rawLocationToSourceLocation(rawLocation); - if (sourceLocation?.lineNumber !== lineNumber) { - throw new Error('Paused at an unexpected location. Have not set a breakpoint here.'); - } - - const variables = await plugin.listVariablesInScope(rawLocation); - expect(variables.map(v => v.name).sort()).to.deep.equal(['n', 'sum', 'x']); - - if (!plugin.evaluate) { - throw new Error('evaluate is undefined'); - } - - { - const {value} = remoteObject(await plugin.evaluate('n + sum', rawLocation, debug.stopIdForCallFrame(callFrame))); - expect(value).to.eql(55); - } - { - const {value} = - remoteObject(await plugin.evaluate('(wchar_t)0x41414141', rawLocation, debug.stopIdForCallFrame(callFrame))); - expect(value).to.eql('U+41414141'); - } - { - const {value} = - remoteObject(await plugin.evaluate('(char16_t)0x4141', rawLocation, debug.stopIdForCallFrame(callFrame))); - expect(value).to.eql('䅁'); - } - { - const {value} = - remoteObject(await plugin.evaluate('(char32_t)0x41414141', rawLocation, debug.stopIdForCallFrame(callFrame))); - expect(value).to.eql('U+41414141'); - } - { - const {value} = - remoteObject(await plugin.evaluate('(char32_t)0x4141', rawLocation, debug.stopIdForCallFrame(callFrame))); - expect(value).to.eql('䅁'); - } - - await debug.resume(); - await debug.close(); - }); -}); diff --git a/test/PathUtils.test.ts b/test/PathUtils.test.ts index f525337..bb483ff 100644 --- a/test/PathUtils.test.ts +++ b/test/PathUtils.test.ts @@ -3,135 +3,55 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/ModuleConfiguration_test.ts -import {findModuleConfiguration, resolveSourcePathToURL} from '../src/ModuleConfiguration.js'; - -describe('findModuleConfiguration', () => { - const CONFIGA = {name: 'projectA.wasm', pathSubstitutions: []}; - const CONFIGB = {name: 'projectB.wasm', pathSubstitutions: []}; - const DEFAULT = {pathSubstitutions: [{from: '/foo', to: '/bar'}]}; - const URLA = new URL('http://localhost/projectA.wasm'); - const URLB = new URL('file:/C:/Users/Admin/Projects/projectB.wasm'); - const URLC = new URL('https://web.dev/projectC.wasm'); - - it('correctly yields named configuration', () => { - expect(findModuleConfiguration([CONFIGA], URLA)).to.be.equal(CONFIGA); - expect(findModuleConfiguration([CONFIGA, CONFIGB], URLA)).to.be.equal(CONFIGA); - expect(findModuleConfiguration([CONFIGB, CONFIGA], URLA)).to.be.equal(CONFIGA); - expect(findModuleConfiguration([DEFAULT, CONFIGB, CONFIGA], URLB)).to.be.equal(CONFIGB); - expect(findModuleConfiguration([CONFIGB, CONFIGA, DEFAULT], URLB)).to.be.equal(CONFIGB); - }); - - it('correctly yields default configuration', () => { - expect(findModuleConfiguration([CONFIGA], URLB)).to.deep.equal({pathSubstitutions: []}); - expect(findModuleConfiguration([CONFIGA, CONFIGB], URLC)).to.deep.equal({pathSubstitutions: []}); - expect(findModuleConfiguration([CONFIGB, CONFIGA], URLC)).to.deep.equal({pathSubstitutions: []}); - expect(findModuleConfiguration([DEFAULT, CONFIGB, CONFIGA], URLC)).to.be.equal(DEFAULT); - expect(findModuleConfiguration([CONFIGB, CONFIGA, DEFAULT], URLC)).to.be.equal(DEFAULT); - }); - - it('correctly matches basename *-patterns', () => { - const STAR_WASM = {name: '*.wasm', pathSubstitutions: []}; - const PROJECTA_STAR = {name: 'projectA.*', pathSubstitutions: []}; - const PROJECT_STAR_WASM = {name: 'project*.wasm', pathSubstitutions: []}; - expect(findModuleConfiguration([PROJECTA_STAR], URLA)).to.be.equal(PROJECTA_STAR); - expect(findModuleConfiguration([STAR_WASM, PROJECTA_STAR], URLA)).to.be.equal(STAR_WASM); - expect(findModuleConfiguration([STAR_WASM, PROJECTA_STAR], new URL('http://example.com/projectA.foo'))) - .to.be.equal(PROJECTA_STAR); - expect(findModuleConfiguration([PROJECT_STAR_WASM], URLA)).to.be.equal(PROJECT_STAR_WASM); - expect(findModuleConfiguration([PROJECT_STAR_WASM], URLB)).to.be.equal(PROJECT_STAR_WASM); - }); - - it('corectly matches **-patterns', () => { - const PROJECT = {name: 'http://localhost/**/*.wasm', pathSubstitutions: []}; - expect(findModuleConfiguration([PROJECT], new URL('http://localhost/file.wasm'))).to.be.equal(PROJECT); - }); -}); - -describe('resolveSourcePathToURL', () => { - it('correctly resolves absolute paths', () => { - const BASE_URL = new URL('http://localhost/file.wasm'); - expect(resolveSourcePathToURL([], '/', BASE_URL).href).to.equal('file:///'); - expect(resolveSourcePathToURL([], '/usr/local', BASE_URL).href).to.equal('file:///usr/local'); - expect(resolveSourcePathToURL([], '/Users/Administrator', BASE_URL).href).to.equal('file:///Users/Administrator'); - expect(resolveSourcePathToURL([], 'A:/', BASE_URL).href).to.equal('file:///A:/'); - expect(resolveSourcePathToURL([], 'c:\\', BASE_URL).href).to.equal('file:///c:/'); - expect(resolveSourcePathToURL([], 'c:\\Users\\Clippy\\Source', BASE_URL).href) - .to.equal('file:///c:/Users/Clippy/Source'); - expect(resolveSourcePathToURL([], '\\\\network\\Server\\Source', BASE_URL).href) - .to.equal('file://network/Server/Source'); - }); - - it('correctly resolves relative paths', () => { - expect(resolveSourcePathToURL([], 'stdint.h', new URL('http://localhost/file.wasm')).href) - .to.equal('http://localhost/stdint.h'); - expect(resolveSourcePathToURL([], 'emscripten/include/iostream', new URL('http://localhost/dist/module.wasm')).href) - .to.equal('http://localhost/dist/emscripten/include/iostream'); - expect(resolveSourcePathToURL([], './src/main.cc', new URL('https://www.example.com/fast.wasm')).href) - .to.equal('https://www.example.com/src/main.cc'); - expect(resolveSourcePathToURL([], '.\\Mein Projekt\\Datei.cpp', new URL('https://www.example.com/fast.wasm')).href) - .to.equal('https://www.example.com/Mein%20Projekt/Datei.cpp'); - }); - - it('correctly applies source path substitutions', () => { - const BASE_URL = new URL('http://localhost/file.wasm'); - expect(resolveSourcePathToURL([{from: '/usr/src', to: '/mnt/src'}], '/usr/include/stdio.h', BASE_URL).href) - .to.equal('file:///usr/include/stdio.h'); - expect(resolveSourcePathToURL([{from: '/usr/src', to: '/mnt/src'}], '/usr/src/include/stdio.h', BASE_URL).href) - .to.equal('file:///mnt/src/include/stdio.h'); - expect( - resolveSourcePathToURL( - [{from: '/usr/src', to: '/mnt/src'}, {from: '/mnt/src', to: '/foo'}], '/usr/src/include/stdio.h', BASE_URL) - .href) - .to.equal('file:///mnt/src/include/stdio.h'); - expect(resolveSourcePathToURL( - [{from: '/usr/src/include', to: '/mnt/include'}, {from: '/usr/src', to: '/mnt/src'}], - '/usr/src/include/stdio.h', BASE_URL) - .href) - .to.equal('file:///mnt/include/stdio.h'); - expect(resolveSourcePathToURL([{from: '.', to: '/srv/central/src'}], './include/string', BASE_URL).href) - .to.equal('file:///srv/central/src/include/string'); - }); - - it('correctly resolves the sidecar Wasm module path', () => { - // We use resolveSourcePathToURL() with an empty source - // map to locate the debugging sidecar Wasm module. - expect(resolveSourcePathToURL([], 'file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href) - .to.equal('http://localhost:8000/wasm/file.wasm.debug.wasm'); - expect( - resolveSourcePathToURL([], '/usr/local/file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')) - .href) - .to.equal('file:///usr/local/file.wasm.debug.wasm'); - expect(resolveSourcePathToURL( - [], 'f:\\netdrive\\file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')) - .href) - .to.equal('file:///f:/netdrive/file.wasm.debug.wasm'); - }); - - it('correctly deals with dot patterns', () => { - // Test that we match LLDB's behavior as implemented for `settings set target.source-map`: - // http://cs/github/llvm/llvm-project/lldb/source/Target/PathMappingList.cpp?l=157-185 - const BASE_URL = new URL('http://web.dev/file.wasm'); - expect(resolveSourcePathToURL([{from: '.', to: '/foo/bar'}], 'include/header.h', BASE_URL).href) - .to.equal('file:///foo/bar/include/header.h'); - expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo\\bar'}], 'include/header.h', BASE_URL).href) - .to.equal('file:///c:/foo/bar/include/header.h'); - expect(resolveSourcePathToURL([{from: '.', to: '/foo'}], '/mnt/main.c', BASE_URL).href) - .to.equal('file:///mnt/main.c'); - expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo'}], '/mnt/main.c', BASE_URL).href) - .to.equal('file:///mnt/main.c'); - expect(resolveSourcePathToURL([{from: '.', to: '/foo'}], 'c:\\mnt\\main.c', BASE_URL).href) - .to.equal('file:///c:/mnt/main.c'); - expect(resolveSourcePathToURL([{from: '.', to: 'c:\\foo'}], 'c:\\mnt\\main.c', BASE_URL).href) - .to.equal('file:///c:/mnt/main.c'); - }); - - it('correctly deals with dot patterns when the source path is a URL', () => { - const BASE_URL = new URL('http://web.dev/file.wasm'); - expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'file:///main.cpp', BASE_URL).href) - .to.equal('file:///main.cpp'); - expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'http://localhost/main.cpp', BASE_URL).href) - .to.equal('http://localhost/main.cpp'); - expect(resolveSourcePathToURL([{from: '.', to: '/bar'}], 'https://localhost/main.cpp', BASE_URL).href) - .to.equal('https://localhost/main.cpp'); - }); -}); +import assert from 'assert'; +import { describe, it } from 'node:test'; + +import { resolveSourcePathToURL } from '../src/PathUtils'; + +describe('PathUtils', () => { + describe('resolveSourcePathToURL', () => { + it('correctly resolves absolute paths', () => { + const BASE_URL = new URL('http://localhost/file.wasm'); + assert.equal(resolveSourcePathToURL('/', BASE_URL).href, 'file:///'); + assert.equal(resolveSourcePathToURL('/usr/local', BASE_URL).href, 'file:///usr/local'); + assert.equal(resolveSourcePathToURL('/Users/Administrator', BASE_URL).href, 'file:///Users/Administrator'); + assert.equal(resolveSourcePathToURL('A:/', BASE_URL).href, 'file:///A:/'); + assert.equal(resolveSourcePathToURL('c:\\', BASE_URL).href, 'file:///c:/'); + assert.equal( + resolveSourcePathToURL('c:\\Users\\Clippy\\Source', BASE_URL).href, + 'file:///c:/Users/Clippy/Source'); + assert.equal( + resolveSourcePathToURL('\\\\network\\Server\\Source', BASE_URL).href, + 'file://network/Server/Source'); + }); + + it('correctly resolves relative paths', () => { + assert.equal( + resolveSourcePathToURL('stdint.h', new URL('http://localhost/file.wasm')).href, + 'http://localhost/stdint.h'); + assert.equal( + resolveSourcePathToURL('emscripten/include/iostream', new URL('http://localhost/dist/module.wasm')).href, + 'http://localhost/dist/emscripten/include/iostream'); + assert.equal( + resolveSourcePathToURL('./src/main.cc', new URL('https://www.example.com/fast.wasm')).href, + 'https://www.example.com/src/main.cc'); + assert.equal( + resolveSourcePathToURL('.\\Mein Projekt\\Datei.cpp', new URL('https://www.example.com/fast.wasm')).href, + 'https://www.example.com/Mein%20Projekt/Datei.cpp'); + }); + + it('correctly resolves the sidecar Wasm module path', () => { + // We use resolveSourcePathToURL() with an empty source + // map to locate the debugging sidecar Wasm module. + assert.equal( + resolveSourcePathToURL('file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href, + 'http://localhost:8000/wasm/file.wasm.debug.wasm'); + assert.equal( + resolveSourcePathToURL('/usr/local/file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href, + 'file:///usr/local/file.wasm.debug.wasm'); + assert.equal( + resolveSourcePathToURL('f:\\netdrive\\file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href, + 'file:///f:/netdrive/file.wasm.debug.wasm'); + }); + }); +}); \ No newline at end of file diff --git a/test/SymbolsBackend.test.ts b/test/SymbolsBackend.test.ts index cf98933..293ecc2 100644 --- a/test/SymbolsBackend.test.ts +++ b/test/SymbolsBackend.test.ts @@ -3,38 +3,48 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/SymbolsBackend_test.ts -import createModule, {type SymbolsBackendTestsModule} from './SymbolsBackendTests.js'; +import assert from 'assert'; +import { describe, it } from 'node:test'; +import { createModuleRunner } from '../test-utils/emscripten-module-runner'; +import type { SymbolsBackendTestsModule } from '../wasm/symbols-backend/tests/SymbolsBackendTests'; + +type ResponseWithError = { error?: { code: string; message: string; }; }; + +function okReponse(response: T) { + if (response.error) { + assert.fail(`Expect succesfull response, got ${response.error.code}: ${response.error.message}`); + } + return response; +} describe('SymbolsBackend', () => { - it('should work', async () => { - await createModule({ - onExit(status: number) { - if (status !== 0) { - throw new Error(`Unittests failed (return code ${status})`); - } - }, + + it('runs WebAssembly test suite', async () => { + const moduleRunner = createModuleRunner('tests/SymbolsBackendTests.js', { stdio: 'inherit' }); + + await moduleRunner.run(createModule => createModule({ // @ts-expect-error - preRun({FS}: SymbolsBackendTestsModule) { // eslint-disable-line @typescript-eslint/naming-convention + preRun({ FS }: SymbolsBackendTestsModule) { FS.mkdir('tests'); FS.mkdir('tests/inputs'); FS.mkdir('cxx_debugging'); FS.mkdir('cxx_debugging/tests'); FS.mkdir('cxx_debugging/tests/inputs'); ['hello.s.wasm', - 'windows_paths.s.wasm', - 'globals.s.wasm', - 'classstatic.s.wasm', - 'namespaces.s.wasm', - 'shadowing.s.wasm', - 'inline.s.wasm', + 'windows_paths.s.wasm', + 'globals.s.wasm', + 'classstatic.s.wasm', + 'namespaces.s.wasm', + 'shadowing.s.wasm', + 'inline.s.wasm', ] - .forEach( - name => FS.createPreloadedFile( - 'cxx_debugging/tests/inputs', name, `build/tests/inputs/${name}`, true, false)); + .forEach( + name => FS.createPreloadedFile( + 'cxx_debugging/tests/inputs', name, `tests/inputs/${name}`, true, false)); ['split-dwarf.s.dwo', - 'split-dwarf.s.wasm', - ].forEach(name => FS.createPreloadedFile('tests/inputs', name, `build/tests/inputs/${name}`, true, false)); - }, - }); + 'split-dwarf.s.wasm', + ].forEach(name => FS.createPreloadedFile('tests/inputs', name, `tests/inputs/${name}`, true, false)); + } + })); }); }); diff --git a/test/mandelbrot.test.ts b/test/WasmWorker.test.ts similarity index 75% rename from test/mandelbrot.test.ts rename to test/WasmWorker.test.ts index 8e2d1ee..924201f 100644 --- a/test/mandelbrot.test.ts +++ b/test/WasmWorker.test.ts @@ -1,4 +1,4 @@ -import * as assert from 'assert'; +import assert from 'assert'; import { afterEach, beforeEach, describe, it } from 'node:test'; import { join } from 'path'; import { pathToFileURL } from 'url'; @@ -6,11 +6,17 @@ import { IWasmWorker, spawn } from '../dist'; // This test with mandelbrot.wasm which was pulled from https://emscripten-dbg-stories.netlify.app/mandelbrot.html -describe('dwarf-debugging', () => { +describe('WasmWorker', () => { let s: IWasmWorker; beforeEach(() => { - s = spawn({} as any); + s = spawn({ + getWasmLinearMemory: async (offset, length, stopId) => new ArrayBuffer(0), + getWasmLocal: async (local, stopId) => ({ type: 'i32', value: 0 }), + getWasmGlobal: async (global, stopId) => ({ type: 'i32', value: 0 }), + getWasmOp: async (op, stopId) => ({ type: 'i32', value: 0 }), + reportResourceLoad: async (resourceUrl, status) => { } + }); }); afterEach(async () => { @@ -18,9 +24,9 @@ describe('dwarf-debugging', () => { }); it('adds a module and reads sources', async () => { - await s.rpc.sendMessage('hello', [], false); + s.rpc.sendMessage('hello', [], false); const r = await s.rpc.sendMessage('addRawModule', 'someId', '', { - url: pathToFileURL(join(__dirname, 'mandelbrot.wasm')).toString(), + url: pathToFileURL(join(__dirname, 'resources/mandelbrot.wasm')).toString(), }); assert.ok(r instanceof Array, 'r is an array'); @@ -34,9 +40,9 @@ describe('dwarf-debugging', () => { let sources: string[]; let mandelbrotcc: string; beforeEach(async () => { - await s.rpc.sendMessage('hello', [], false); + s.rpc.sendMessage('hello', [], false); const r = await s.rpc.sendMessage('addRawModule', 'someId', '', { - url: pathToFileURL(join(__dirname, 'mandelbrot.wasm')).toString(), + url: pathToFileURL(join(__dirname, 'resources/mandelbrot.wasm')).toString(), }); sources = r as string[]; mandelbrotcc = sources.find((r) => r.endsWith('mandelbrot.cc'))!; diff --git a/test/e2e/e2e-specs.test.ts b/test/e2e/e2e-specs.test.ts new file mode 100644 index 0000000..1987340 --- /dev/null +++ b/test/e2e/e2e-specs.test.ts @@ -0,0 +1,245 @@ +import assert from 'assert'; +import fs from 'fs'; +import yaml from 'js-yaml'; +import { after, before, describe, it } from 'node:test'; +import path from 'path'; +import { Chrome } from '../../src/ExtensionAPI'; +import DebuggerSession from '../../test-utils/DebuggerSession'; +import { createModuleRunner } from '../../test-utils/emscripten-module-runner'; + +const DEBUGGER_PORT = 9231; +const TEST_SPECS_FOLDER = path.resolve(`${__dirname}/../../wasm/e2e`); +const TEST_BUILD_FOLDER = path.resolve(`${__dirname}/../../wasm/e2e.build`); + +interface TestSpec { + name: string; + only?: boolean; + skip?: boolean; + source_file: string; + use_dwo?: boolean; + flags: string[][]; + script: { + reason: string; + file?: string; + line?: number; + actions?: { + action: string; + file?: string; + breakpoint?: number; + }[]; + variables?: { + name: string; + type?: string; + value?: unknown; + }[]; + evaluations?: { + expression: string; + value: unknown; + }[]; + }[]; +} + +const testFiles = fs.readdirSync(TEST_SPECS_FOLDER) + .filter(file => file.endsWith('.yaml')) + .map(file => ({ file, test: loadTestSpecFile(path.join(TEST_SPECS_FOLDER, file)) })); + +describe(`e2e specs (${TEST_SPECS_FOLDER})`, { only: testFiles.find(({ test }) => test.only)?.test.only }, () => { + for (const { file, test } of testFiles) { + const { name, only, skip, source_file, flags } = test; + + describe(`${name} (file: ${file}, source_file: ${source_file})`, { only, skip }, () => { + for (let i = 0; i < flags.length; i++) { + const outputName = testCaseOutputName(source_file, name, i); + describe(`flags: ${flags[i].join(' ')}`, { only, skip }, async () => { + let debuggerSession: DebuggerSession; + let moduleExited: Promise; + let failed: boolean; + before(async () => { + const module = createModuleRunner(`${outputName}.js`, { debuggerPort: DEBUGGER_PORT, cwd: TEST_BUILD_FOLDER }); + moduleExited = module.run(); + debuggerSession = await DebuggerSession.attach(DEBUGGER_PORT, `${TEST_BUILD_FOLDER}/${outputName}.wasm`); + failed = false; + }); + defineTestCaseScript( + test, + (description, fn) => it( + Object.entries(description) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => `${key}: ${value}`) + .join(', '), + { only, skip }, + async context => { + if (failed) { + context.skip(); + return; + } + try { + await fn(debuggerSession); + } catch (e) { + failed = true; + throw e; + } + } + ), + ); + after(async () => { + await debuggerSession.dispose(); + await moduleExited; + }); + }); + } + }); + } +}); + +async function defineTestCaseScript(test: TestSpec, testCallback: (description: Record, fn: (debuggerSession: DebuggerSession) => Promise) => void) { + + let breakpoints: Map; + let isFirstStep: boolean; + let isResumed: boolean; + + before(() => { + breakpoints = new Map(); + isFirstStep = true; + isResumed = true; + }); + + for (const { reason, file, line, variables = [], evaluations = [], actions = [] } of test.script) { + testCallback({ reason, file, line }, async (debuggerSession) => { + if (!isResumed) { + await debuggerSession.resume(); + } + const { hitBreakpoints, sourceFileURL = '', lineNumber } = await debuggerSession.waitForPaused(); + switch (reason) { + case 'setup': { + assert(isFirstStep, `Reason 'setup' must be first step.`); + break; + } + case 'breakpoint': { + const breakpointId = breakpoints.get(`${file}:${line}`); + assert(breakpointId, 'Breakpoint not set.'); + assert(hitBreakpoints, `Paused because of reason: ${reason}`); + assert(hitBreakpoints.includes(breakpointId), `Paused at other breakpoint: ${hitBreakpoints.join(', ')}`); + break; + } + case 'step': { + assert(reason === 'step', `Paused because of reason: ${reason}`); + assert(sourceFileURL.endsWith(`/${file}`), `Paused in file: ${sourceFileURL}`); + assert(lineNumber === line, `Paused at line: ${lineNumber}`); + break; + } + } + isFirstStep = false; + isResumed = false; + }); + + for (const { name, type, value } of variables || []) { + testCallback({ variable: name, type, value }, async (debuggerSession) => { + const result = await getVariable(debuggerSession, name); + assert(value === undefined || result.value === `${value}` || result.description === `${value}`, `Actual value: ${result.value}`); + assert(type === undefined || result.description === type, `Actual type: '${result.description}'`); + }); + } + + for (const { expression, value } of evaluations) { + testCallback({ expression, value }, async (debuggerSession) => { + const result = formatEvaluateResult(await debuggerSession.evaluate(expression)); + assert(result.value === `${value}`, `Actual value: ${result.value}`); + }); + } + + for (const { action, breakpoint, file = path.basename(test.source_file) } of actions) { + testCallback({ action, file, breakpoint }, async (debuggerSession) => { + assert(!isResumed); + switch (action) { + case 'set_breakpoint': { + assert(breakpoint, `No breakpoint specified.`); + assert(!breakpoints.has(`${file}:${breakpoint}`), `Breakpoint is already set.`); + const breakpointId = await debuggerSession.addBreakpoint(file, breakpoint); + breakpoints.set(`${file}:${breakpoint}`, breakpointId); + break; + } + case 'remove_breakpoint': { + assert(breakpoint, `No breakpoint specified.`); + const breakpointId = breakpoints.get(`${file}:${breakpoint}`); + assert(breakpointId, `Breakpoint is not set at.`); + await debuggerSession.removeBreakpoint(breakpointId); + break; + } + case 'step_over': { + await debuggerSession.stepOver(); + isResumed = true; + break; + } + case 'step_into': { + await debuggerSession.stepInto(); + isResumed = true; + break; + } + case 'step_out': { + await debuggerSession.stepOut(); + isResumed = true; + break; + } + } + }); + } + } +} + +function loadTestSpecFile(filePath: string): TestSpec { + return yaml.load(fs.readFileSync(filePath, 'utf-8')) as TestSpec; +} + +function testCaseOutputName(sourceFilePath: string, name: string, i: number) { + return `${path.basename(sourceFilePath, path.extname(sourceFilePath))}__${name}_${i}`.replace(/[^0-9a-zA-Z]+/g, '_'); +} + +async function getVariable(debuggerSession: DebuggerSession, scopedPath: string) { + const [scope, name, ...propertiesPath] = scopedPath.split('.'); + assert(scope); + assert(name); + + const variableList = await debuggerSession.listVariablesInScope(); + assert(variableList.some(v => v.name === name && v.scope === scope.toUpperCase()), `Variable ${scope}.${name} does not exist.`); + + let value = await debuggerSession.evaluate(name); + let parent = `${scope}.${name}`; + for (const name of propertiesPath) { + assert( + value && + 'type' in value && + (value.type === 'object' || value.type === 'array') && + value.objectId, + `Expected value for '${parent}' to be of type object.`, + ); + + const propertiesList = await debuggerSession.getProperties(value.objectId); + const property = /^\$\d+$/.test(name) + ? propertiesList[parseInt(name.slice(1))] + : propertiesList.find(p => p.name === name); + assert(property, `Variable ${parent}.${name} does not exist.`); + + value = property.value; + parent = `${parent}.${name}`; + } + + return formatEvaluateResult(value); +} + +function formatEvaluateResult(result: Chrome.DevTools.RemoteObject | Chrome.DevTools.ForeignObject | null): { value: string, description?: string; } { + if (result == null) { + return { value: 'null' }; + } + if ('type' in result && 'valueClass' in result) { + return { + value: `${result.valueClass}.${result.index}`, + description: result.type + }; + } + return { + value: `${result.value}`, + description: result.description + }; +} + diff --git a/test/e2e/lldb-eval.test.ts b/test/e2e/lldb-eval.test.ts new file mode 100644 index 0000000..e0b97a1 --- /dev/null +++ b/test/e2e/lldb-eval.test.ts @@ -0,0 +1,101 @@ +// This file is based on a file from revision cf3a5c70b97b388fff3490b62f1bdaaa6a26f8e1 +// of the Chrome DevTools C/C++ Debugging Extension, see the wasm/symbols-backend/LICENSE file. +// +// https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/Interpreter_test.ts + +import assert from 'assert'; +import { describe, it } from 'node:test'; +import { Chrome } from '../../src/ExtensionAPI'; +import DebuggerSession from '../../test-utils/DebuggerSession'; +import { createModuleRunner, DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD } from '../../test-utils/emscripten-module-runner'; +import type { LLDBEvalTestsModule } from '../../wasm/symbols-backend/tests/LLDBEvalTests'; + +const DEBUGGER_PORT = 9232; +const ADDRESSES_FILE = 'wasm/symbols-backend/tests/inputs/addresses.cc'; + +describe('lldb-eval', () => { + + it('runs WebAssembly test suite', async () => { + + const module = createModuleRunner('tests/LLDBEvalTests.js', { imports: ['tsx'], stdio: 'inherit' }); + + await module.run(async loadModule => { + // We are executing from 'test-utils/emscripten-module-runner/child-process.js' so we need to adjust the paths accordingly + const { LLDBEvalDebugger } = require('../../test-utils/LLDBEvalDebugger') as typeof import('../../test-utils/LLDBEvalDebugger'); + const lldbEval = await loadModule(); + const debug = new LLDBEvalDebugger(); + + const argv = new lldbEval.StringArray(); + try { + const skippedTests = [ + 'EvalTest.TestTemplateTypes', + 'EvalTest.TestUnscopedEnumNegation', + 'EvalTest.TestUniquePtrDeref', + 'EvalTest.TestUniquePtrCompare', + ]; + argv.push_back(`--gtest_filter=-${skippedTests.join(':')}`); + + const exitCode = await lldbEval.runTests(debug, argv); + if (exitCode !== 0) { + throw new Error('gtest test suite failed'); + } + } finally { + argv.delete(); + } + }); + }); + + it('can do basic arithmetic.', async () => { + + const module = createModuleRunner('tests/inputs/addresses_main.js', { imports: ['tsx'], debuggerPort: DEBUGGER_PORT }); + const moduleExited = module.run(); + + const debuggerSession = await DebuggerSession.attach( + DEBUGGER_PORT, + `${DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD}/tests/inputs/addresses_main.wasm`, + `${DEFAULT_EMSCRIPTEN_MODULE_RUNNER_CWD}/tests/inputs/addresses_main.wasm.debug.wasm`, + ); + try { + + await debuggerSession.continueToLine(ADDRESSES_FILE, '// BREAK(ArrayMembersTest)'); + await debuggerSession.waitForPaused(); + + const variables = await debuggerSession.listVariablesInScope(); + assert.deepEqual(variables.map(v => v.name).sort(), ['n', 'sum', 'x']); + + { + const { value } = remoteObject(await debuggerSession.evaluate('n + sum')); + assert.equal(value, 55); + } + { + const { value } = + remoteObject(await debuggerSession.evaluate('(wchar_t)0x41414141')); + assert.equal(value, 'U+41414141'); + } + { + const { value } = + remoteObject(await debuggerSession.evaluate('(char16_t)0x4141')); + assert.equal(value, '䅁'); + } + { + const { value } = + remoteObject(await debuggerSession.evaluate('(char32_t)0x41414141')); + assert.equal(value, 'U+41414141'); + } + { + const { value } = + remoteObject(await debuggerSession.evaluate('(char32_t)0x4141')); + assert.equal(value, '䅁'); + } + } finally { + await debuggerSession.dispose(); + await moduleExited; + } + }); +}); + +export function remoteObject(value: Chrome.DevTools.RemoteObject | Chrome.DevTools.ForeignObject | null): Chrome.DevTools.RemoteObject { + assert(value); + assert(value.type !== 'reftype'); + return value; +} diff --git a/test/mandelbrot.wasm b/test/resources/mandelbrot.wasm similarity index 100% rename from test/mandelbrot.wasm rename to test/resources/mandelbrot.wasm diff --git a/tsconfig.json b/tsconfig.json index 3e387a2..04be7ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,21 @@ { "compilerOptions": { "strict": true, - "module": "CommonJS", - "target": "ES2021" - } -} + "noImplicitAny": true, + "target": "ESNext", + "module": "NodeNext", + "lib": [ + "WebWorker" + ], + "allowUnreachableCode": false, + "noEmitOnError": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "sourceMap": true, + "outDir": "dist" + }, + "include": [ + "src/*", + "tests/*" + ] +} \ No newline at end of file diff --git a/wasm/build-e2e.sh b/wasm/build-e2e.sh new file mode 100644 index 0000000..03299f7 --- /dev/null +++ b/wasm/build-e2e.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +build_e2e() { + SOURCE_DIR="$(pwd)/$1" + E2E_BUILD="$(pwd)/$2" + + mkdir -p "$E2E_BUILD" + + ./generate-e2e-resources-build "$SOURCE_DIR"/*.yaml > "$E2E_BUILD/build.ninja" + + ninja -C "$E2E_BUILD" +} diff --git a/wasm/build-stage-1.sh b/wasm/build-stage-1.sh new file mode 100755 index 0000000..0bdc776 --- /dev/null +++ b/wasm/build-stage-1.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +build_stage_1() { + SOURCE_DIR="$(pwd)/$1" + STAGE1_BUILD="$(pwd)/$2" + + mkdir -p "$STAGE1_BUILD" + + cmake \ + -S "$SOURCE_DIR" \ + -B "$STAGE1_BUILD" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -GNinja \ + -DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-unknown-unknown \ + -DLLVM_ENABLE_ZLIB=OFF \ + -DLLVM_ENABLE_TERMINFO=OFF \ + -DLLDB_ENABLE_CURSES=OFF \ + -DLLDB_ENABLE_LIBXML2=OFF \ + -DLLDB_ENABLE_LZMA=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_C_COMPILER="${HOST_C_COMPILER:-"clang"}" \ + -DCMAKE_CXX_COMPILER="${HOST_CXX_COMPILER:-"clang++"}" + + ninja -C "$STAGE1_BUILD" lldb-tblgen clang-tblgen llvm-tblgen llvm-dwp llvm-mc +} diff --git a/wasm/build-stage-2.sh b/wasm/build-stage-2.sh new file mode 100755 index 0000000..763e349 --- /dev/null +++ b/wasm/build-stage-2.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -e + +build_stage_2() { + SOURCE_DIR="$(pwd)/$1" + STAGE2_BUILD="$(pwd)/$2" + STAGE1_TOOLS="$(pwd)/$3/third_party/llvm/src/llvm/bin" + + mkdir -p "$STAGE2_BUILD" + + cmake \ + -S "$SOURCE_DIR" \ + -B "$STAGE2_BUILD" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -GNinja \ + -DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-unknown-unknown \ + -DLLVM_ENABLE_ZLIB=OFF \ + -DLLVM_ENABLE_TERMINFO=OFF \ + -DLLDB_ENABLE_CURSES=OFF \ + -DLLDB_ENABLE_LIBXML2=OFF \ + -DLLDB_ENABLE_LZMA=OFF \ + -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="-O1 -g -DNDEBUG" \ + -DCMAKE_C_FLAGS_RELWITHDEBINFO="-O1 -g -DNDEBUG" \ + -DCMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO="-O1 -g -DNDEBUG -gseparate-dwarf" \ + -DCMAKE_CXX_FLAGS_DEBUG="-O0 -g -DNDEBUG " \ + -DCMAKE_EXE_LINKER_FLAGS_DEBUG="-O0 -g -gseparate-dwarf " \ + -DHAVE_POSIX_REGEX=0 \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \ + -DLLVM_DWP="$STAGE1_TOOLS/llvm-dwp" \ + -DLLVM_TABLEGEN="$STAGE1_TOOLS/llvm-tblgen" \ + -DCLANG_TABLEGEN="$STAGE1_TOOLS/clang-tblgen" \ + -DLLDB_TABLEGEN="$STAGE1_TOOLS/lldb-tblgen" \ + -DWASM_LD="$EMSDK/upstream/bin/wasm-ld" \ + -DCXX_DEBUGGING_USE_SPLIT_DWARF=OFF \ + -DLLVM_USE_SPLIT_DWARF=OFF \ + -DCXX_DEBUGGING_ENABLE_DWARF5=OFF \ + -DCXX_DEBUGGING_ENABLE_PUBNAMES=OFF \ + -DCXX_DEBUGGING_DWO_ONLY=OFF \ + -DCXX_DEBUGGING_USE_SANITIZERS=OFF + + ninja -C "$STAGE2_BUILD" -j10 all +} diff --git a/wasm/build.sh b/wasm/build.sh new file mode 100755 index 0000000..dac77a3 --- /dev/null +++ b/wasm/build.sh @@ -0,0 +1,40 @@ +#!/bin/sh -e +# shellcheck disable=SC1091 +set -e +cd "$(dirname "$0")" + +SCRIPT_DIR="$(pwd)" + +if [ -z "$EMSDK" ]; then + echo "ERROR: Environment variable EMSDK must be set" + exit 1 +fi + +cd "$EMSDK" +. ./emsdk_env.sh + +cd "$SCRIPT_DIR" + +. ./fetch-git.sh +. ./build-stage-1.sh +. ./build-stage-2.sh +. ./build-e2e.sh + +fetch symbols-backend/third_party/llvm/src \ + https://github.com/llvm/llvm-project.git \ + 3c51ea3619e488db19cd26840ed46d58cfc7062f + +fetch "symbols-backend/third_party/lldb-eval/src" \ + https://github.com/google/lldb-eval.git \ + e87123a7e639bf1d86f24c37079570fb7fa00b72 + +build_stage_1 symbols-backend \ + symbols-backend.build/stage-1 + +build_stage_2 symbols-backend \ + symbols-backend.build/stage-2 \ + symbols-backend.build/stage-1 + +build_e2e e2e e2e.build + +cp symbols-backend.build/stage-2/compile_commands.json symbols-backend diff --git a/wasm/builder/Dockerfile b/wasm/builder/Dockerfile new file mode 100644 index 0000000..0e91eec --- /dev/null +++ b/wasm/builder/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20 + +WORKDIR / + +# Install python, ninja and cmake +RUN apt-get update && apt-get install -y python3 git cmake ninja-build + +# Install emsdk +RUN git config --global init.defaultBranch main +RUN git clone https://github.com/emscripten-core/emsdk.git +WORKDIR /emsdk +ENV EMSDK_VERSION=3.1.14 +ENV EMSDK=/emsdk +RUN ./emsdk install "$EMSDK_VERSION" && ./emsdk activate "$EMSDK_VERSION" + +ENV HOST_C_COMPILER=/emsdk/upstream/bin/clang +ENV HOST_CXX_COMPILER=/emsdk/upstream/bin/clang++ + +WORKDIR /wasm + +# Install node modules +RUN npm i js-yaml@4.1.0 + +ENTRYPOINT [ "/bin/sh", "build.sh" ] \ No newline at end of file diff --git a/wasm/e2e/cpp_eval.yaml b/wasm/e2e/cpp_eval.yaml index 321dcf8..2a16b35 100644 --- a/wasm/e2e/cpp_eval.yaml +++ b/wasm/e2e/cpp_eval.yaml @@ -4,12 +4,12 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/cpp_eval.yaml name: Cpp expression evaluation -source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c +source_file: resources/stepping-with-state.c flags: [ [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] -use_dwo: +use_dwo: true script: - reason: setup actions: @@ -35,4 +35,4 @@ script: - expression: 1 == 1 value: true - expression: (char)65 - value: "\"'A'\"" + value: "'A'" diff --git a/wasm/e2e/pointer.yaml b/wasm/e2e/pointer.yaml index 6b5f3ab..098d4e5 100644 --- a/wasm/e2e/pointer.yaml +++ b/wasm/e2e/pointer.yaml @@ -4,10 +4,10 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/pointer.yaml name: Test Pointer Chains -source_file: //extensions/cxx_debugging/e2e/resources/pointers.cc +source_file: resources/pointers.cc flags: [ - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] script: diff --git a/wasm/e2e/resources/stepping-with-state.c b/wasm/e2e/resources/stepping-with-state.c index 2c5b635..09d28d1 100644 --- a/wasm/e2e/resources/stepping-with-state.c +++ b/wasm/e2e/resources/stepping-with-state.c @@ -12,7 +12,7 @@ void multiplyByConstant(int *array, int length, int constant) { } int main() { - int n = 10; + const int n = 10; int x[n]; /* initialize x */ diff --git a/wasm/e2e/scope_view_non_primitives.yaml b/wasm/e2e/scope_view_non_primitives.yaml index 2475135..1a92acf 100644 --- a/wasm/e2e/scope_view_non_primitives.yaml +++ b/wasm/e2e/scope_view_non_primitives.yaml @@ -6,7 +6,7 @@ name: Scope view formats non primitive types correctly # This test checks that the scope view displays # arrays, structs and pointers correctly -source_file: //extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.c +source_file: resources/scope-view-non-primitives.c flags: [ [-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], diff --git a/wasm/e2e/scope_view_non_primitives_cpp.yaml b/wasm/e2e/scope_view_non_primitives_cpp.yaml index ac88a1e..fbd737c 100644 --- a/wasm/e2e/scope_view_non_primitives_cpp.yaml +++ b/wasm/e2e/scope_view_non_primitives_cpp.yaml @@ -7,9 +7,9 @@ name: Scope view formats non primitive types correctly for cpp # This test checks that the scope view displays # arrays, structs, references, pointers and # classes correctly for C++ -source_file: //extensions/cxx_debugging/e2e/resources/scope-view-non-primitives.cpp +source_file: resources/scope-view-non-primitives.cpp flags: [ - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] diff --git a/wasm/e2e/scope_view_primitives.yaml b/wasm/e2e/scope_view_primitives.yaml index d678d18..85baad6 100644 --- a/wasm/e2e/scope_view_primitives.yaml +++ b/wasm/e2e/scope_view_primitives.yaml @@ -6,7 +6,7 @@ name: Scope view formats primitive types correctly # This test checks that the scope view displays # integers, chars, float and doubles correctly -source_file: //extensions/cxx_debugging/e2e/resources/scope-view-primitives.c +source_file: resources/scope-view-primitives.c flags: [ [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], @@ -25,7 +25,7 @@ script: - name: Local.i value: 10 - name: Local.c - value: "\"'a'\"" + value: "'a'" - name: Local.f value: 1.10000002 - name: Local.d diff --git a/wasm/e2e/stepping_with_state.yaml b/wasm/e2e/stepping_with_state.yaml index c0360fb..8fcda20 100644 --- a/wasm/e2e/stepping_with_state.yaml +++ b/wasm/e2e/stepping_with_state.yaml @@ -4,10 +4,10 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/stepping_with_state.yaml name: Stepping with state -source_file: //extensions/cxx_debugging/e2e/resources/stepping-with-state.c +source_file: resources/stepping-with-state.c flags: [ [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] script: @@ -22,7 +22,7 @@ script: variables: - name: Local.n value: 10 - - name: Local.x + - name: Local.x.$0 value: 0 actions: - action: step_over @@ -32,7 +32,7 @@ script: variables: - name: Local.n value: 10 - - name: Local.x + - name: Local.x.$0 value: 0 actions: - action: remove_breakpoint @@ -46,7 +46,7 @@ script: variables: - name: Local.n value: 10 - - name: Local.x + - name: Local.x.$0 value: 0 actions: - action: step_over @@ -56,7 +56,7 @@ script: variables: - name: Local.n value: 10 - - name: Local.x + - name: Local.x.$0 value: 0 - name: Local.i value: 0 diff --git a/wasm/e2e/string_view.yaml b/wasm/e2e/string_view.yaml index 473034b..2b3e8be 100644 --- a/wasm/e2e/string_view.yaml +++ b/wasm/e2e/string_view.yaml @@ -4,9 +4,9 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/string_view.yaml name: Test String View -source_file: //extensions/cxx_debugging/tests/inputs/string_view.cc +source_file: ../symbols-backend/tests/inputs/string_view.cc flags: [ - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] diff --git a/wasm/e2e/test_big_dwo.yaml b/wasm/e2e/test_big_dwo.yaml index e71dd60..78e3585 100644 --- a/wasm/e2e/test_big_dwo.yaml +++ b/wasm/e2e/test_big_dwo.yaml @@ -4,8 +4,8 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_big_dwo.yaml name: Successfully loads large dwo files -source_file: //extensions/cxx_debugging/e2e/resources/huge-source-file.cc -use_dwo: +source_file: resources/huge-source-file.cc +use_dwo: true flags: [ [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] script: diff --git a/wasm/e2e/test_loop.yaml b/wasm/e2e/test_loop.yaml index 5072d9a..1931611 100644 --- a/wasm/e2e/test_loop.yaml +++ b/wasm/e2e/test_loop.yaml @@ -4,7 +4,7 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_loop.yaml name: test_loop -source_file: //third_party/emscripten-releases/install/emscripten/tests/core/test_loop.c +source_file: $EMSDK/upstream/emscripten/tests/core/test_loop.c flags: [[-g, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK]] script: - reason: setup diff --git a/wasm/e2e/test_wasm_simd.yaml b/wasm/e2e/test_wasm_simd.yaml index d4abf74..ef2c39a 100644 --- a/wasm/e2e/test_wasm_simd.yaml +++ b/wasm/e2e/test_wasm_simd.yaml @@ -4,7 +4,7 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/test_wasm_simd.yaml name: test_wasm_simd -source_file: //extensions/cxx_debugging/e2e/resources/test_wasm_simd.c +source_file: resources/test_wasm_simd.c flags: [ [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -msimd128, -fdebug-compilation-dir=., -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], diff --git a/wasm/e2e/vector.yaml b/wasm/e2e/vector.yaml index 70233a7..89b0468 100644 --- a/wasm/e2e/vector.yaml +++ b/wasm/e2e/vector.yaml @@ -4,7 +4,7 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/vector.yaml name: Test std::vector -source_file: //extensions/cxx_debugging/e2e/resources/vector.cc +source_file: resources/vector.cc flags: [ [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], diff --git a/wasm/e2e/wchar.yaml b/wasm/e2e/wchar.yaml index 4a850f8..e5e2699 100644 --- a/wasm/e2e/wchar.yaml +++ b/wasm/e2e/wchar.yaml @@ -4,9 +4,9 @@ # https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/e2e/tests/wchar.yaml name: Test Wide-Character Strings -source_file: //extensions/cxx_debugging/e2e/resources/wchar.cc +source_file: resources/wchar.cc flags: [ - [-g, -fdebug-compilation-dir=., -fno-limit-debug-info -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], + [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -fno-limit-debug-info, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK], [-g, -fdebug-compilation-dir=., -gdwarf-5, -sWASM_BIGINT, -sERROR_ON_WASM_CHANGES_AFTER_LINK, -gsplit-dwarf, -gpubnames]] @@ -30,7 +30,11 @@ script: - name: Local.short_cxx_str.string value: '"a"' - name: Local.c_str + type: 'const wchar_t *' + - name: Local.c_str.string value: '"abcde"' + - name: Local.c_str.size + value: 5 - name: Local.u16_cxx_str.size value: 5 - name: Local.u16_cxx_str.string @@ -40,7 +44,11 @@ script: - name: Local.u16_short_cxx_str.string value: '"a"' - name: Local.u16_c_str + type: 'const char16_t *' + - name: Local.u16_c_str.string value: '"abcde"' + - name: Local.u16_c_str.size + value: 5 - name: Local.u32_cxx_str.size value: 5 - name: Local.u32_cxx_str.string @@ -50,4 +58,8 @@ script: - name: Local.u32_short_cxx_str.string value: '"a"' - name: Local.u32_c_str + type: 'const char32_t *' + - name: Local.u32_c_str.size + value: 5 + - name: Local.u32_c_str.string value: '"abcde"' diff --git a/wasm/fetch-git.sh b/wasm/fetch-git.sh new file mode 100644 index 0000000..0e6e759 --- /dev/null +++ b/wasm/fetch-git.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +fetch() { + TARGET_DIR="$1" + PREVIOUS_DIR="$(pwd)" + URL="$2" + REV="$3" + + if [ -d "$TARGET_DIR/.git" ] + then + cd "$TARGET_DIR" + else + mkdir -p "$TARGET_DIR" + cd "$TARGET_DIR" + git init + git remote add origin "$URL" + fi + + LOCAL_REV="$(git rev-parse HEAD 2> /dev/null || true)" + + if [ "$REV" != "$LOCAL_REV" ] + then + echo "Fetching thirdparty source for $TARGET_DIR from $URL ($REV) ..." + git fetch origin "$REV" && \ + git reset --hard FETCH_HEAD + echo "Done." + fi + + cd "$PREVIOUS_DIR" +} + + + diff --git a/wasm/generate-e2e-resources-build b/wasm/generate-e2e-resources-build new file mode 100755 index 0000000..3462a3f --- /dev/null +++ b/wasm/generate-e2e-resources-build @@ -0,0 +1,71 @@ +#!/usr/local/bin/node +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const out = process.stdout.write.bind(process.stdout); +const err = process.stderr.write.bind(process.stderr); + +const inputFiles = process.argv.slice(2); + +out(`rule build_dwp\n`); +out(` command = ${process.env['EMSDK']}/upstream/bin/llvm-dwp $in -o $out\n`); +out(` description = Generating dwp file, creating $out\n\n`); +out(`rule build_wasm\n`); +out(` command = $compiler $flags $in -o $out\n`); +out(` description = Compiling file '$in' with flags: "$flags"\n\n`); + +for (const specFilePath of inputFiles) { + const { name, source_file: sourceFile, flags, use_dwo: useDwo } = yaml.load(fs.readFileSync(specFilePath, 'utf8')); + + assert(specFilePath, typeof name === 'string', `Expected top level string property 'name'`); + assert(specFilePath, typeof sourceFile === 'string', `Expected top level string property 'source_file'`); + assert(specFilePath, Array.isArray(flags) && flags.every(Array.isArray), `Expected top level array of arrays property 'flags'`); + + const sourceFilePath = resolvePath(specFilePath, sourceFile); + assert(specFilePath, fs.existsSync(sourceFilePath), `File does not exist: ${sourceFile}`); + + const sourceFileIsCpp = ['.cc', '.cpp'].some(extname => extname === path.extname(sourceFilePath).toLowerCase()); + + for (let i = 0; i < flags.length; i++) { + const outputFile = testCaseOutputName(sourceFilePath, name, i); + const compiler = `${process.env['EMSDK']}/upstream/emscripten/${sourceFileIsCpp ? 'em++' : 'emcc'}`; + const compileFlags = flags[i].filter(f => !f.startsWith('-s')).join(' '); + const linkFlags = ['-sMODULARIZE=1', '-sEXIT_RUNTIME=1', '-sENVIRONMENT=node', ...flags[i]].join(' '); + + const hasSplitDwarf = flags[i].includes('-gsplit-dwarf'); + const additionalObjOutput = hasSplitDwarf ? ` | ${outputFile}.dwo` : ''; + + out(`build ${outputFile}.js | ${outputFile}.wasm: build_wasm ${outputFile}.o\n`); + out(` compiler = ${compiler}\n`); + out(` flags = ${linkFlags}\n\n`); + out(`build ${outputFile}.o${additionalObjOutput}: build_wasm ${encodeFilePath(sourceFilePath)}\n`); + out(` compiler = ${compiler} -c\n`); + out(` flags = ${compileFlags}\n\n`); + if (hasSplitDwarf && !useDwo) { + out(`build ${outputFile}.wasm.dwp: build_dwp ${outputFile}.dwo\n\n`); + } + } +} + +function resolvePath(specFilePath, filePath) { + return path.resolve( + path.dirname(specFilePath), + filePath.replace(/\$([A-Z_]+)/g, (_, v) => process.env[v] || ''), + ); +} + +function encodeFilePath(filePath) { + return filePath.replace(/\$|\s/g, s => '$' + s); +} + +function assert(specFilePath, condition, message) { + if (!condition) { + err(`[${specFilePath}]ERROR: ${message}\n`); + process.exit(-1); + } +} + +function testCaseOutputName(sourceFilePath, name, i) { + return `${path.basename(sourceFilePath, path.extname(sourceFilePath))}__${name}_${i}`.replace(/[^0-9a-zA-Z]+/g, '_'); +} diff --git a/wasm/symbols-backend/CMakeLists.txt b/wasm/symbols-backend/CMakeLists.txt index 3713d12..b86d561 100644 --- a/wasm/symbols-backend/CMakeLists.txt +++ b/wasm/symbols-backend/CMakeLists.txt @@ -20,40 +20,6 @@ set(CXX_DEBUGGING_SOURCE_DIR ${PROJECT_SOURCE_DIR}) option(CXX_DEBUGGING_USE_SANITIZERS "Enable sanitizers" OFF) -# Compile typescript sources -find_program(TS_COMPILER tsc PATHS ${DEVTOOLS_SOURCE_DIR}/node_modules/.bin REQUIRED NO_DEFAULT_PATH) - -set(TS_COMPILER_ARGS -p ${CMAKE_CURRENT_SOURCE_DIR} --outDir ${PROJECT_BINARY_DIR}) -exec_program(${TS_COMPILER} ${CMAKE_CURRENT_SOURCE_DIR} - ARGS ${TS_COMPILER_ARGS} --listFiles - OUTPUT_VARIABLE TS_COMPILER_INPUTS - RETURN_VALUE TS_COMPILER_RETVAL) - -if (NOT ${TS_COMPILER_RETVAL} EQUAL 0) - message(FATAL_ERROR "Running tsc failed:\n${TS_COMPILER_INPUTS}") -endif() - -string(REPLACE "\n" ";" TS_COMPILER_INPUTS ${TS_COMPILER_INPUTS}) - -set(TS_COMPILER_OUTPUTS) -foreach(tsc_input IN LISTS TS_COMPILER_INPUTS) - get_filename_component(ext ${tsc_input} EXT) - if (NOT ext MATCHES ".d.ts$") - file(RELATIVE_PATH rel_path ${CMAKE_CURRENT_SOURCE_DIR} ${tsc_input}) - get_filename_component(basename ${rel_path} NAME_WE) - get_filename_component(dirname ${rel_path} DIRECTORY) - list(APPEND TS_COMPILER_OUTPUTS ${CMAKE_CURRENT_BINARY_DIR}/${dirname}/${basename}.js) - endif() -endforeach() - -add_custom_command(OUTPUT ${TS_COMPILER_OUTPUTS} - COMMAND ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py --output - ${TS_COMPILER} ${TS_COMPILER_ARGS} - COMMENT "Compiling Typescript" - DEPENDS ${TS_COMPILER_INPUTS} tsconfig.json) - -add_custom_target(TypescriptOutput DEPENDS ${TS_COMPILER_OUTPUTS}) - option(CXX_DEBUGGING_ENABLE_DWARF5 "Enable -gdwarf-5 for emitting DWARF5" OFF) if(CXX_DEBUGGING_ENABLE_DWARF5) add_compile_options(-gdwarf-5) @@ -102,6 +68,28 @@ else() set(LLVM_USE_SANITIZER "") endif() +# llvm/cmake/config-ix.cmake imports 'Platform/${CMAKE_HOST_SYSTEM_NAME}' which used to work on +# earlier versions of CMake however newer versions does not support importing the 'Platform/${CMAKE_HOST_SYSTEM_NAME}' +# files directly. +# We work around this by including a copy of the _cmake_record_install_prefix function +# from 'CMakeSystemSpecificInformation.cmake' here. +function(_cmake_record_install_prefix ) + set(_CMAKE_SYSTEM_PREFIX_PATH_INSTALL_PREFIX_VALUE "${CMAKE_INSTALL_PREFIX}" PARENT_SCOPE) + set(_CMAKE_SYSTEM_PREFIX_PATH_STAGING_PREFIX_VALUE "${CMAKE_STAGING_PREFIX}" PARENT_SCOPE) + set(icount 0) + set(scount 0) + foreach(value IN LISTS CMAKE_SYSTEM_PREFIX_PATH) + if(value STREQUAL CMAKE_INSTALL_PREFIX) + math(EXPR icount "${icount}+1") + endif() + if(value STREQUAL CMAKE_STAGING_PREFIX) + math(EXPR scount "${scount}+1") + endif() + endforeach() + set(_CMAKE_SYSTEM_PREFIX_PATH_INSTALL_PREFIX_COUNT "${icount}" PARENT_SCOPE) + set(_CMAKE_SYSTEM_PREFIX_PATH_STAGING_PREFIX_COUNT "${scount}" PARENT_SCOPE) +endfunction() + add_subdirectory(${THIRD_PARTY_DIR}/llvm/src/llvm ${CMAKE_CURRENT_BINARY_DIR}/third_party/llvm/src/llvm) set_property(DIRECTORY ${THIRD_PARTY_DIR}/llvm/src/llvm PROPERTY EXCLUDE_FROM_ALL TRUE) @@ -116,25 +104,6 @@ endif() # Required to enable llvm option parsing include(${THIRD_PARTY_DIR}/llvm/src/llvm/cmake/modules/DetermineGCCCompatible.cmake) -macro(copy_file INPUT OUTPUT) - get_filename_component(ABS_INPUT ${INPUT} ABSOLUTE BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) - get_filename_component(ABS_OUTPUT ${OUTPUT} ABSOLUTE BASE_DIR ${CMAKE_CURRENT_BINARY_DIR}) - add_custom_command(OUTPUT ${OUTPUT} - DEPENDS ${INPUT} - COMMAND ${CMAKE_COMMAND} -E copy ${ABS_INPUT} ${ABS_OUTPUT} - ) -endmacro() - -function(fix_sysroot TARGET) - if (CMAKE_SYSROOT) - target_link_libraries(${TARGET} PRIVATE - -Wl,-rpath,${CMAKE_SYSROOT}/usr/lib/x86_64-linux-gnu - -Wl,-rpath,${CMAKE_SYSROOT}/lib/x86_64-linux-gnu - -Wl,-dynamic-linker,${CMAKE_SYSROOT}/lib64/ld-linux-x86-64.so.2 - ) - endif() -endfunction() - set(LLVM_RUNTIME_OUTPUT_INTDIR ${LLVM_BINARY_DIR}/${CMAKE_CFG_INTDIR}/bin) set(LLVM_LIBRARY_OUTPUT_INTDIR ${LLVM_BINARY_DIR}/${CMAKE_CFG_INTDIR}/lib${LLVM_LIBDIR_SUFFIX}) set(LLVM_TOOLS_BINARY_DIR ${LLVM_RUNTIME_OUTPUT_INTDIR}) @@ -155,12 +124,6 @@ if(CXX_DEBUGGING_USE_SPLIT_DWARF) endif() endif() -fix_sysroot(lldb-tblgen) -fix_sysroot(clang-tblgen) -fix_sysroot(llvm-tblgen) -fix_sysroot(llvm-mc) -fix_sysroot(llvm-dwp) - if (CXX_DEBUGGING_BUILD_WASM) add_subdirectory(lib) add_subdirectory(src) diff --git a/wasm/symbols-backend/src/CMakeLists.txt b/wasm/symbols-backend/src/CMakeLists.txt index 6ee4c01..4677416 100644 --- a/wasm/symbols-backend/src/CMakeLists.txt +++ b/wasm/symbols-backend/src/CMakeLists.txt @@ -2,63 +2,6 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -set(DEV_RESOURCES - TestDriver.js - DevToolsPluginForTests.html - index.html - ) - -set(EXTENSION_RESOURCES - DevToolsPlugin.html - ExtensionOptions.html - ) - -foreach(EXTENSION_RESOURCE IN LISTS EXTENSION_RESOURCES DEV_RESOURCES) - copy_file(${EXTENSION_RESOURCE} ${EXTENSION_RESOURCE}) -endforeach(EXTENSION_RESOURCE) -configure_file(manifest.json.in manifest.json @ONLY) - -set(EXTENSION_BUNDLE_ENTRYPOINTS - DevToolsPluginHost.js - DevToolsPluginWorkerMain.js - ExtensionOptions.js -) -if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") - set(EXTENSION_BUNDLE_SOURCEMAP TRUE) -else() - set(EXTENSION_BUNDLE_SOURCEMAP FALSE) -endif() -set(EXTENSION_BUNDLE_FORMAT "es") - -set(EXTENSION_BUNDLED_SOURCES) -foreach(entrypoint IN LISTS EXTENSION_BUNDLE_ENTRYPOINTS) - get_filename_component(NAME_WE ${entrypoint} NAME_WE) - list(APPEND EXTENSION_BUNDLED_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${NAME_WE}.bundle.js) -endforeach() - -configure_file(rollup.config.in.js rollup.config.in.js @ONLY) -file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.js - INPUT ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.in.js) - -add_custom_command( - OUTPUT - ${EXTENSION_BUNDLED_SOURCES} - COMMAND - ${CMAKE_COMMAND} - ARGS - -E env NODE_PATH=${PROJECT_SOURCE_DIR}/node_modules:${DEVTOOLS_SOURCE_DIR}/node_modules - python3 - ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py - --output - ${DEVTOOLS_SOURCE_DIR}/node_modules/.bin/rollup - -c --failAfterWarnings - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - MAIN_DEPENDENCY ${CMAKE_CURRENT_BINARY_DIR}/rollup.config.js - DEPENDS ${TS_COMPILER_OUTPUTS} -) - -add_custom_target(Bundles DEPENDS ${EXTENSION_BUNDLED_SOURCES}) - add_executable(SymbolsBackend SymbolsBackend.cc) target_link_libraries(SymbolsBackend PUBLIC DWARFSymbols) target_include_directories(SymbolsBackend PUBLIC @@ -83,6 +26,7 @@ if (CXX_DEBUGGING_USE_SANITIZERS) -fsanitize=address,undefined ) endif() + if (NOT CMAKE_BUILD_TYPE STREQUAL "Release") target_link_libraries(SymbolsBackend PRIVATE -sERROR_ON_WASM_CHANGES_AFTER_LINK @@ -93,8 +37,7 @@ else() target_link_libraries(SymbolsBackend PRIVATE -sASSERTIONS=0) endif() -target_link_libraries(SymbolsBackend PRIVATE -s'ENVIRONMENT=web,worker') -target_link_libraries(SymbolsBackend PRIVATE -s'EXPORT_ES6=1') +target_link_libraries(SymbolsBackend PRIVATE -s'ENVIRONMENT=node') add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/SymbolsBackend.wasm.debug.wasm.dwp @@ -127,7 +70,6 @@ endif() set(CXX_DEBUGGING_ARCHIVE "${PROJECT_BINARY_DIR}/cxx_debugging_extension-${CMAKE_PROJECT_VERSION}.zip") set(CXX_DEBUGGING_INPUTS ${EXTENSION_BUNDLED_SOURCES} - ${CMAKE_CURRENT_BINARY_DIR}/manifest.json $) foreach(RESOURCE IN LISTS EXTENSION_RESOURCES) @@ -141,13 +83,13 @@ set(CXX_DEBUGGING_DIST_FILES add_custom_command(OUTPUT "${CXX_DEBUGGING_ARCHIVE}" COMMAND ${CMAKE_COMMAND} -E tar "cf" "${CXX_DEBUGGING_ARCHIVE}" --format=zip -- ${CXX_DEBUGGING_DIST_FILES} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${CXX_DEBUGGING_INPUTS} Bundles + DEPENDS ${CXX_DEBUGGING_INPUTS} COMMENT "Zipping to ${CXX_DEBUGGING_ARCHIVE}.") if(CMAKE_BUILD_TYPE STREQUAL "Release") - add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_ARCHIVE} ${DEV_RESOURCES}) + add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_ARCHIVE}) else() - add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_INPUTS} ${DEV_RESOURCES}) + add_custom_target(DevToolsPlugin ALL DEPENDS ${CXX_DEBUGGING_INPUTS}) endif() add_custom_command(TARGET DevToolsPlugin POST_BUILD diff --git a/wasm/symbols-backend/tests/CMakeLists.txt b/wasm/symbols-backend/tests/CMakeLists.txt index 8179092..553b7a4 100644 --- a/wasm/symbols-backend/tests/CMakeLists.txt +++ b/wasm/symbols-backend/tests/CMakeLists.txt @@ -2,19 +2,6 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. - -add_custom_target(check-extension - COMMAND - ${DEVTOOLS_SOURCE_DIR}/third_party/node/node.py - --output - ${DEVTOOLS_SOURCE_DIR}/node_modules/karma/bin/karma start - ${CMAKE_CURRENT_BINARY_DIR}/karma.conf.js - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - DEPENDS SymbolsBackendTests SymbolsBackend LLDBEvalTests - ${TS_COMPILER_OUTPUTS} - ${EXTENSION_BUNDLED_SOURCES} - ) - include_directories( ${REPO_SOURCE_DIR}/third_party/llvm/src/llvm/utils/unittest/googlemock/include ${REPO_SOURCE_DIR}/third_party/llvm/src/llvm/utils/unittest/googletest/include) @@ -44,12 +31,10 @@ target_link_libraries(SymbolsBackendTests PRIVATE DWARFSymbols -sALLOW_MEMORY_GROWTH=1 -sMODULARIZE=1 - -sENVIRONMENT=web + -sENVIRONMENT=node -sEXPORT_NAME=createModule -sEXIT_RUNTIME=1 - -sEXPORT_ES6=1 -s'EXTRA_EXPORTED_RUNTIME_METHODS=[\"FS\"]' - -s'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=[\"$$Browser\"]' -sWASM_BIGINT -sERROR_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS=1 @@ -84,16 +69,16 @@ set(compile_options -include ${CMAKE_CURRENT_SOURCE_DIR}/LLDBEvalExtensions.h ${incflags} ) + set(link_options - -sASYNCIFY - -sASYNCIFY_STACK_SIZE=49152 -sWASM_BIGINT -std=c++17 --bind -g0 -sEXPORT_NAME=loadModule - -sEXPORT_ES6=1 -sASSERTIONS=1 + -sENVIRONMENT=node + -sMODULARIZE=1 ) if (CXX_DEBUGGING_USE_SANITIZERS) list(APPEND link_options @@ -114,10 +99,9 @@ get_target_property(link_options LLDBEvalTests list(APPEND compiled_inputs ${CMAKE_CURRENT_BINARY_DIR}/LLDBEvalTests.js) list(APPEND compiled_wasm_inputs ${CMAKE_CURRENT_BINARY_DIR}/LLDBEvalTests.wasm) -add_dependencies(SymbolsBackendTests SymbolsBackendTestInputs TypescriptOutput) +add_dependencies(SymbolsBackendTests SymbolsBackendTestInputs) set(EXTENSION_TEST_BUILD_ARTIFACTS - ${TS_COMPILER_OUTPUTS} ${EXTENSION_BUNDLED_SOURCES} $ $/LLDBEvalTests.wasm @@ -136,11 +120,3 @@ set(EXTENSION_TEST_BUILD_ARTIFACTS endif() add_subdirectory(inputs) - -configure_file(build-artifacts.js.in build-artifacts.js.in @ONLY) -file(GENERATE OUTPUT build-artifacts.js - INPUT ${CMAKE_CURRENT_BINARY_DIR}/build-artifacts.js.in) - -configure_file(karma.conf.in.js karma.conf.in.js @ONLY) -file(GENERATE OUTPUT karma.conf.js - INPUT ${CMAKE_CURRENT_BINARY_DIR}/karma.conf.in.js) diff --git a/wasm/symbols-backend/tests/LLDBEvalExtensions.h b/wasm/symbols-backend/tests/LLDBEvalExtensions.h index 820bd21..8ab2f74 100644 --- a/wasm/symbols-backend/tests/LLDBEvalExtensions.h +++ b/wasm/symbols-backend/tests/LLDBEvalExtensions.h @@ -54,14 +54,13 @@ class EvalTest : public ::testing::Test { std::string test_name = ::testing::UnitTest::GetInstance()->current_test_info()->name(); std::string break_line = "// BREAK(" + test_name + ")"; - debugger.call("runToLine", break_line).await(); + debugger.call("runToLine", break_line); } - void TearDown() { debugger.call("exit").await(); } + void TearDown() { debugger.call("exit"); } - EvalResult Eval(const std::string& expr) const { - const auto evalResult = - debugger.call("evaluate", expr).await(); + EvalResult Eval(const std::string &expr) const { + const auto evalResult = debugger.call("evaluate", expr); const auto result = evalResult["result"]; const auto error = evalResult["error"]; return {result.as() ? result.as() : std::string(), diff --git a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts index fa810ba..8190551 100644 --- a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts +++ b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts @@ -1,12 +1,12 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the wasm/symbols-backend/LICENSE file. -import type {Vector} from '../src/SymbolsBackend.js'; +import type { Vector } from '../../../src/SymbolsBackend'; export interface Debugger { - runToLine(line: string): Promise; - evaluate(expr: string): Promise; - exit(): Promise; + runToLine(line: string): void; + evaluate(expr: string): EvalResult; + exit(): void; } export interface EvalResult { @@ -14,11 +14,11 @@ export interface EvalResult { result?: string; } -interface Module extends EmscriptenModule { +export interface LLDBEvalTestsModule extends EmscriptenModule { // eslint-disable-next-line @typescript-eslint/naming-convention StringArray: Vector; - runTests(dbg: Debugger, args: Vector): Promise; + runTests(dbg: Debugger, args: Vector): number; } -declare let loadModule: EmscriptenModuleFactory; +declare let loadModule: EmscriptenModuleFactory; export default loadModule; diff --git a/wasm/symbols-backend/tests/inputs/CMakeLists.txt b/wasm/symbols-backend/tests/inputs/CMakeLists.txt index eb02a79..a0081b2 100644 --- a/wasm/symbols-backend/tests/inputs/CMakeLists.txt +++ b/wasm/symbols-backend/tests/inputs/CMakeLists.txt @@ -76,8 +76,8 @@ macro(add_test_program program) -sMODULARIZE=1 -sWASM_BIGINT -sEXPORT_NAME=loadModule - -sEXPORT_ES6=1 -sEXPORT_ALL=1 + -sENVIRONMENT=node -std=c++17 -Wl,--export-all -Wl,--no-gc-sections @@ -121,21 +121,8 @@ add_test_program(lldb_eval_inputs ${THIRD_PARTY_DIR}/lldb-eval/src/testdata/test_binary.cc ${THIRD_PARTY_DIR}/lldb-eval/src/testdata/test_library.cc) - -set(TEST_BINARY_INPUTS - page.html - page.js - externref.js - ) - -set(binary_inputs) -foreach(input IN LISTS TEST_BINARY_INPUTS) - list(APPEND binary_inputs ${CMAKE_CURRENT_BINARY_DIR}/${input}) - add_custom_command(OUTPUT ${input} DEPENDS ${input} - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${input} ${CMAKE_CURRENT_BINARY_DIR}/) -endforeach() -add_custom_target(SymbolsBackendTestInputs DEPENDS ${compiled_inputs} ${TEST_BINARY_INPUTS}) +add_custom_target(SymbolsBackendTestInputs DEPENDS ${compiled_inputs}) add_custom_command(TARGET SymbolsBackendTestInputs POST_BUILD COMMAND rm ${missing_dwos}) -list(APPEND EXTENSION_TEST_BUILD_ARTIFACTS ${compiled_inputs} ${binary_inputs} ${compiled_wasm_inputs}) +list(APPEND EXTENSION_TEST_BUILD_ARTIFACTS ${compiled_inputs} ${compiled_wasm_inputs}) set(EXTENSION_TEST_BUILD_ARTIFACTS ${EXTENSION_TEST_BUILD_ARTIFACTS} PARENT_SCOPE) From 7195f4063e4a84fb7700c719424bd0d2da85d859 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Mon, 28 Apr 2025 16:12:05 +1000 Subject: [PATCH 5/9] Fix https://issues.chromium.org/issues/401522579 `TypeError: Failed to construct URL: Invalid URL` thrown for certain source code paths in the DW_AT_decl_file tags causing all source code resolving to fail, not just the specific source code path. --- src/PathUtils.ts | 23 +++++++++++++++-------- test/PathUtils.test.ts | 7 +++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/PathUtils.ts b/src/PathUtils.ts index 38e21ae..c05cbd4 100644 --- a/src/PathUtils.ts +++ b/src/PathUtils.ts @@ -19,14 +19,21 @@ export function resolveSourcePathToURL(sourcePath: string, baseURL: URL): URL { // Normalize '\' to '/' in sourcePath first. const resolvedSourcePath = sourcePath.replace(/\\/g, '/'); - if (resolvedSourcePath.startsWith('/')) { - if (resolvedSourcePath.startsWith('//')) { - return new URL(`file:${resolvedSourcePath}`); + try { + if (resolvedSourcePath.startsWith('/')) { + if (resolvedSourcePath.startsWith('//')) { + return new URL(`file:${resolvedSourcePath}`); + } + return new URL(`file://${resolvedSourcePath}`); } - return new URL(`file://${resolvedSourcePath}`); - } - if (/^[A-Z]:/i.test(resolvedSourcePath)) { - return new URL(`file:/${resolvedSourcePath}`); + if (/^[A-Z]:/i.test(resolvedSourcePath)) { + return new URL(`file:/${resolvedSourcePath}`); + } + return new URL(resolvedSourcePath, baseURL.href); + } catch (e) { + if (e instanceof TypeError && 'code' in e && e.code === 'ERR_INVALID_URL') { + return new URL(`file://${resolvedSourcePath.replace(/\/+/g, '/')}`); + } + throw e; } - return new URL(resolvedSourcePath, baseURL.href); } diff --git a/test/PathUtils.test.ts b/test/PathUtils.test.ts index bb483ff..b6369c0 100644 --- a/test/PathUtils.test.ts +++ b/test/PathUtils.test.ts @@ -53,5 +53,12 @@ describe('PathUtils', () => { resolveSourcePathToURL('f:\\netdrive\\file.wasm.debug.wasm', new URL('http://localhost:8000/wasm/file.wasm')).href, 'file:///f:/netdrive/file.wasm.debug.wasm'); }); + + it('gracefully deals with invalid host names in URL as if it was a file on localhost instead', () => { + const BASE_URL = new URL('http://web.dev/file.wasm'); + assert.equal( + resolveSourcePathToURL('//v24.0/build/sysroot/wasi-libc-wasm32-wasip1/dlmalloc/include/unistd.h', BASE_URL).href, + 'file:///v24.0/build/sysroot/wasi-libc-wasm32-wasip1/dlmalloc/include/unistd.h'); + }); }); }); \ No newline at end of file From 39098556b3d8e8705cca353c0bd7022c8e1c7216 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Mon, 28 Apr 2025 16:20:57 +1000 Subject: [PATCH 6/9] Add support for Rust types - Extend TypeSystemClang so we can handle Rust types like variants and zero sized marker types - Simplify unmarshalling of embind objects (replace EmbindObjectPool with getUnmarshalledInterface for easier handling responses from SymbolsBackend) - Add E2E spec file support for Rust --- README.md | 24 +- src/CustomFormatters.ts | 175 ++++-- src/DWARFLanguageExtensionPlugin.ts | 512 ++++++------------ src/Formatters.ts | 153 +++++- src/SymbolsBackend.d.ts | 59 +- src/embind.ts | 113 ++++ test-utils/SymbolsBackendPlugin.ts | 85 +++ test-utils/TestValue.ts | 78 ++- test-utils/TestWasmInterface.ts | 12 +- .../emscripten-module-runner/child-process.js | 5 +- .../emscripten-module-runner/prelude.js | 2 + test/Formatters.test.ts | 261 ++++++++- test/SymbolsBackend.test.ts | 98 +++- test/e2e/e2e-specs.test.ts | 9 +- wasm/builder/Dockerfile | 14 +- wasm/e2e/resources/app.rs | 65 +++ wasm/e2e/rust_app.yaml | 131 +++++ wasm/generate-e2e-resources-build | 25 + wasm/symbols-backend/lib/ApiContext.cc | 95 +++- wasm/symbols-backend/lib/ApiContext.h | 3 +- wasm/symbols-backend/lib/Variables.cc | 1 - wasm/symbols-backend/lib/WasmModule.cc | 4 - wasm/symbols-backend/lib/WasmModule.h | 2 + wasm/symbols-backend/lib/WasmVendorPlugins.cc | 360 +++++++++++- wasm/symbols-backend/lib/WasmVendorPlugins.h | 128 +++++ wasm/symbols-backend/lib/api.h | 110 +++- wasm/symbols-backend/src/SymbolsBackend.cc | 36 +- wasm/symbols-backend/tests/LLDBEvalTests.d.ts | 3 +- 28 files changed, 2056 insertions(+), 507 deletions(-) create mode 100644 src/embind.ts create mode 100644 test-utils/SymbolsBackendPlugin.ts create mode 100644 test-utils/emscripten-module-runner/prelude.js create mode 100644 wasm/e2e/resources/app.rs create mode 100644 wasm/e2e/rust_app.yaml diff --git a/README.md b/README.md index 4671724..ba59bf7 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,31 @@ This repository publishes an npm module which is used by [js-debug](https://gith This project works by compiling the WebAssembly backend for the Chrome [C/C++ Debugging Extension](https://github.com/ChromeDevTools/devtools-frontend/tree/main/extensions/cxx_debugging) in a way that is usable by Node.js programs. Fortunately, the extension is architected such that most work is done inside a WebWorker, and porting the TypeScript code to instead run in a Node worker_thread is not terribly difficult. Appropriate TypeScript types are exposed, and this module is then shimmed into js-debug which 'pretends' to be Devtools. +In addition for the variable view and the basic `C/C++` expression evaluation support via [lldb-eval](https://github.com/google/lldb-eval) included in the original Chrome C/C++ Debugging Extension the following features has been added: + + - Basic support for `Rust` types, most specifically sum types which couldn't be viewed in the original extension but also better support for core library types like `Vec` and `String` which were partially viewable in the original extension. + + ## Contributing -### Building +### Building (using Docker or Podman) 1. Clone this repo, and have Docker or Podman installed. -2. Run `npm run build`. This will take a while (~1hr depending on how fast your computer is.) Note that for developing you can run individual build steps in their appropriate scripts. -3. You then have a built module. Run `npm run test` to run some basic tests. +2. Run `npm i` +3. Run `npm run build`. This will take a while (~1hr depending on how fast your computer is.) Note that for developing you can run individual build steps in their appropriate scripts. +4. You then have a built module. Run `npm run test` to run the test suite. + +### Building (using local install of build tools and a posix compatible shell) + +1. Clone this repo, and have the following dependencies installed on your machine: + - `cmake`, `python3` and `ninja-build` + - [Emscripten SDK](https://github.com/emscripten-core/emsdk.git) installed, environment variable `EMSDK` pointing to the root folder of the SDK and version `3.1.14` activated. + - `nodejs v20+` + - `rust v1.83+` with target `wasm32-wasip1` added (for compiling parts of the test suite) +2. Run `npm i` +3. Run `wasm/build.sh && npm run build-meta`. This will take a while but probably shorter than if building with Docker or Podman. +4. You then have a built module. Run `npm run test` to run the test suite. + ### General diff --git a/src/CustomFormatters.ts b/src/CustomFormatters.ts index ec4e7dc..2cfbc34 100644 --- a/src/CustomFormatters.ts +++ b/src/CustomFormatters.ts @@ -10,8 +10,8 @@ import type { HostInterface } from './WorkerRPC'; export interface FieldInfo { typeId: string; - name: string | undefined; offset: number; + name?: string; } export interface Enumerator { @@ -20,20 +20,48 @@ export interface Enumerator { value: bigint; } +export interface VariantInfo { + discriminatorValue?: bigint; + members: FieldInfo[]; +} + +export interface VariantPartInfo { + discriminatorMember: FieldInfo; + variants: VariantInfo[]; +} + +export interface TemplateParameterInfo { + typeId: string; + name?: string; +} + +export interface ExtendedTypeInfo { + languageId: number; + variantParts: VariantPartInfo[]; + templateParameters: TemplateParameterInfo[]; +} + export interface TypeInfo { + typeNames: string[]; typeId: string; - enumerators?: Enumerator[]; alignment: number; size: number; + canExpand: boolean; + hasValue: boolean; + arraySize: number; isPointer: boolean; members: FieldInfo[]; - arraySize: number; - hasValue: boolean; - typeNames: string[]; - canExpand: boolean; + enumerators: Enumerator[]; + extendedInfo?: ExtendedTypeInfo; +} + +export interface TypeInfoTree { + root: TypeInfo; + typeInfos: TypeInfo[]; } export interface WasmInterface { + readonly view: WasmMemoryView; readMemory(offset: number, length: number): Uint8Array; getOp(op: number): WasmValue; getLocal(local: number): WasmValue; @@ -41,21 +69,25 @@ export interface WasmInterface { } export interface Value { - location: number; - size: number; - typeNames: string[]; - asUint8: () => number; - asUint16: () => number; - asUint32: () => number; - asUint64: () => bigint; - asInt8: () => number; - asInt16: () => number; - asInt32: () => number; - asInt64: () => bigint; - asFloat32: () => number; - asFloat64: () => number; - asDataView: (offset?: number, size?: number) => DataView; - $: (member: string | number) => Value; + readonly location: number; + readonly size: number; + readonly typeNames: string[]; + readonly isEnumeratedValue: boolean; + readonly extendedTypeInfo?: ExtendedTypeInfo; + asUint8(): number; + asUint16(): number; + asUint32(): number; + asUint64(): bigint; + asInt8(): number; + asInt16(): number; + asInt32(): number; + asInt64(): bigint; + asFloat32(): number; + asFloat64(): number; + asDataView(offset?: number, size?: number): DataView; + asType(typeId: string, offset?: number): Value; + asPointerToType(typeId: string): Value; + $(member: string | number): Value; getMembers(): string[]; } @@ -282,10 +314,10 @@ export class CXXValue implements Value, LazyObject { this.objectStore = objectStore; this.objectId = objectStore.store(this); this.displayValue = displayValue; - this.memoryAddress = memoryAddress; + this.memoryAddress = memoryAddress === undefined && !data ? this.location : memoryAddress; } - static create(objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, typeInfo: { + static create(objectStore: LazyObjectStore, wasm: WasmInterface, memoryView: WasmMemoryView, value: { typeInfos: TypeInfo[], root: TypeInfo, location?: number, @@ -294,10 +326,10 @@ export class CXXValue implements Value, LazyObject { memoryAddress?: number, }): CXXValue { const typeMap = new Map(); - for (const info of typeInfo.typeInfos) { + for (const info of value.typeInfos) { typeMap.set(info.typeId, info); } - const { location, root, data, displayValue, memoryAddress } = typeInfo; + const { location, root, data, displayValue, memoryAddress } = value; return new CXXValue(objectStore, wasm, memoryView, location ?? 0, root, typeMap, data, displayValue, memoryAddress); } @@ -331,10 +363,10 @@ export class CXXValue implements Value, LazyObject { const properties = []; if (this.type.arraySize > 0) { for (let index = 0; index < this.type.arraySize; ++index) { - properties.push({ name: `${index}`, property: await this.getArrayElement(index) }); + properties.push({ name: `${index}`, property: this.getArrayElement(index) }); } } else { - const members = await this.members; + const members = this.members; const data = members.has('*') ? undefined : this.data; for (const [name, { location, type }] of members) { const property = new CXXValue(this.objectStore, this.wasm, this.memoryView, location, type, this.typeMap, data); @@ -345,30 +377,25 @@ export class CXXValue implements Value, LazyObject { } async asRemoteObject(): Promise { - if (this.type.hasValue && this.type.arraySize === 0) { - const formatter = CustomFormatters.get(this.type); - if (!formatter) { - const type = 'undefined' as Chrome.DevTools.RemoteObjectType; - const description = ''; - return { type, description, hasChildren: false }; - } - + const formatter = CustomFormatters.get(this.type); + if (formatter) { if (this.location === undefined || (!this.data && this.location === 0xffffffff)) { const type = 'undefined' as Chrome.DevTools.RemoteObjectType; const description = ''; return { type, description, hasChildren: false }; } - const value = - new CXXValue(this.objectStore, this.wasm, this.memoryView, this.location, this.type, this.typeMap, this.data); - try { - const formattedValue = await formatter.format(this.wasm, value); + const formattedValue = formatter.format(this.wasm, this); return await lazyObjectFromAny( formattedValue, this.objectStore, this.type, this.displayValue, this.memoryAddress) .asRemoteObject(); } catch { // Fallthrough } + } else if (this.type.hasValue && this.type.arraySize === 0) { + const type = 'undefined' as Chrome.DevTools.RemoteObjectType; + const description = ''; + return { type, description, hasChildren: false }; } const type = (this.type.arraySize > 0 ? 'array' : 'object') as Chrome.DevTools.RemoteObjectType; @@ -383,12 +410,20 @@ export class CXXValue implements Value, LazyObject { }; } + get size(): number { + return this.type.size; + } + get typeNames(): string[] { return this.type.typeNames; } - get size(): number { - return this.type.size; + get isEnumeratedValue(): boolean { + return this.type.enumerators.length > 0; + } + + get extendedTypeInfo(): ExtendedTypeInfo | undefined { + return this.type.extendedInfo; } asInt8(): number { @@ -434,6 +469,54 @@ export class CXXValue implements Value, LazyObject { } return this.memoryView.asDataView(offset, size); } + asType(typeId: string, offset?: number): CXXValue { + const targetType = this.typeMap.get(typeId); + if (!targetType) { + throw new Error(`Invalid type id ${typeId}`); + } + const targetData = offset && this.data !== undefined + ? this.data.slice(offset, offset + targetType.size) + : this.data; + return new CXXValue( + this.objectStore, + this.wasm, + this.memoryView, + this.location + (offset || 0), + targetType, + this.typeMap, + targetData, + this.displayValue, + this.memoryAddress + ); + } + asPointerToType(typeId: string): CXXValue { + const targetType = this.typeMap.get(typeId); + if (!targetType) { + throw new Error(`Invalid type id ${typeId}`); + } + return new CXXValue( + this.objectStore, + this.wasm, + this.memoryView, + this.location, + { + typeNames: targetType.typeNames.map(t => `${t}${t.endsWith('*') ? '' : ' '}*`), + typeId: '', + alignment: 4, + size: 4, + canExpand: true, + hasValue: true, + arraySize: 0, + isPointer: true, + members: [{ name: '*', typeId, offset: 0 }], + enumerators: [], + }, + this.typeMap, + this.data, + this.displayValue, + this.memoryAddress + ); + } $(selector: string | number): CXXValue { const data = this.members.has('*') ? undefined : this.data; @@ -471,7 +554,7 @@ export function primitiveObject( value: T, description?: string, linearMemoryAddress?: number, type?: TypeInfo): PrimitiveLazyObject | null { if (['number', 'string', 'boolean', 'bigint', 'undefined'].includes(typeof value)) { if (typeof value === 'bigint' || typeof value === 'number') { - const enumerator = type?.enumerators?.find(e => e.value === BigInt(value)); + const enumerator = type?.enumerators.find(e => e.value === BigInt(value)); if (enumerator) { description = enumerator.name; } @@ -596,14 +679,12 @@ export type FormatterCallback = (wasm: WasmInterface, value: Value) => Formatter export interface Formatter { types: string[] | ((t: TypeInfo) => boolean); - imports?: FormatterCallback[]; format: FormatterCallback; } -export class HostWasmInterface { +export class HostWasmInterface implements WasmInterface { private readonly hostInterface: HostInterface; private readonly stopId: unknown; - private readonly cache: Chrome.DevTools.ForeignObject[] = []; readonly view: WasmMemoryView; constructor(hostInterface: HostInterface, stopId: unknown) { this.hostInterface = hostInterface; @@ -625,9 +706,9 @@ export class HostWasmInterface { } export class DebuggerProxy { - wasm: HostWasmInterface; + wasm: WasmInterface; target: EmscriptenModule; - constructor(wasm: HostWasmInterface, target: EmscriptenModule) { + constructor(wasm: WasmInterface, target: EmscriptenModule) { this.wasm = wasm; this.target = target; } diff --git a/src/DWARFLanguageExtensionPlugin.ts b/src/DWARFLanguageExtensionPlugin.ts index 8754bb4..486cd2b 100644 --- a/src/DWARFLanguageExtensionPlugin.ts +++ b/src/DWARFLanguageExtensionPlugin.ts @@ -8,20 +8,12 @@ import './Formatters'; import type { Chrome } from './ExtensionAPI'; import * as Formatters from './CustomFormatters'; +import { getUnmarshalledInterface, UnmarshalInterface } from './embind'; import { resolveSourcePathToURL } from './PathUtils'; import type * as SymbolsBackend from './SymbolsBackend'; import createSymbolsBackend from './SymbolsBackend'; import type { HostInterface } from './WorkerRPC'; -function mapVector(vector: SymbolsBackend.Vector, callback: (apiElement: ApiT) => T): T[] { - const elements: T[] = []; - for (let i = 0; i < vector.size(); ++i) { - const element = vector.get(i); - elements.push(callback(element)); - } - return elements; -} - interface ScopeInfo { type: 'GLOBAL' | 'LOCAL' | 'PARAMETER'; typeName: string; @@ -30,92 +22,36 @@ interface ScopeInfo { type LazyFSNode = FS.FSNode & { contents: { cacheLength: () => void, length: number; }; }; -function mapEnumerator(apiEnumerator: SymbolsBackend.Enumerator): Formatters.Enumerator { - return { typeId: apiEnumerator.typeId, value: apiEnumerator.value, name: apiEnumerator.name }; -} - -function mapFieldInfo(apiFieldInfo: SymbolsBackend.FieldInfo): Formatters.FieldInfo { - return { typeId: apiFieldInfo.typeId, offset: apiFieldInfo.offset, name: apiFieldInfo.name }; -} - class ModuleInfo { readonly fileNameToUrl: Map; readonly urlToFileName: Map; - readonly dwarfSymbolsPlugin: SymbolsBackend.DWARFSymbolsPlugin; + readonly dwarfSymbolsPlugin: UnmarshalInterface; constructor( readonly symbolsUrl: string, readonly symbolsFileName: string, readonly symbolsDwpFileName: string | undefined, readonly backend: SymbolsBackend.Module) { this.fileNameToUrl = new Map(); this.urlToFileName = new Map(); - this.dwarfSymbolsPlugin = new backend.DWARFSymbolsPlugin(); - } - - stringifyScope(scope: SymbolsBackend.VariableScope): 'GLOBAL' | 'LOCAL' | 'PARAMETER' { - switch (scope) { - case this.backend.VariableScope.GLOBAL: - return 'GLOBAL'; - case this.backend.VariableScope.LOCAL: - return 'LOCAL'; - case this.backend.VariableScope.PARAMETER: - return 'PARAMETER'; - } - throw new Error(`InternalError: Invalid scope ${scope}`); - } - - stringifyErrorCode(errorCode: SymbolsBackend.ErrorCode): string { - switch (errorCode) { - case this.backend.ErrorCode.PROTOCOL_ERROR: - return 'ProtocolError:'; - case this.backend.ErrorCode.MODULE_NOT_FOUND_ERROR: - return 'ModuleNotFoundError:'; - case this.backend.ErrorCode.INTERNAL_ERROR: - return 'InternalError'; - case this.backend.ErrorCode.EVAL_ERROR: - return 'EvalError'; - } - throw new Error(`InternalError: Invalid error code ${errorCode}`); - } -} - -function createEmbindPool(): { - flush(): void, - manage(object: T): T, - unmanage(object: T): boolean, -} { - class EmbindObjectPool { - private objectPool: SymbolsBackend.EmbindObject[] = []; - - flush(): void { - for (const object of this.objectPool.reverse()) { - object.delete(); - } - this.objectPool = []; - } - - manage(object: T): T { - if (typeof object !== 'undefined') { - this.objectPool.push(object); - } - return object; - } - - unmanage(object: T): boolean { - const index = this.objectPool.indexOf(object); - if (index > -1) { - this.objectPool.splice(index, 1); - object.delete(); - return true; - } - return false; - } + this.dwarfSymbolsPlugin = getUnmarshalledInterface(new backend.DWARFSymbolsPlugin(), { + methods: [ + 'AddRawModule', + 'RemoveRawModule', + 'SourceLocationToRawLocation', + 'RawLocationToSourceLocation', + 'ListVariablesInScope', + 'GetFunctionInfo', + 'GetInlinedFunctionRanges', + 'GetInlinedCalleesRanges', + 'GetMappedLines', + 'EvaluateExpression', + 'delete', + ], + enumTypes: [ + backend.ErrorCode, + backend.VariableScope + ], + }); } - - const pool = new EmbindObjectPool(); - const manage = pool.manage.bind(pool); - const unmanage = pool.unmanage.bind(pool); - const flush = pool.flush.bind(pool); - return { manage, unmanage, flush }; } // Cache the underlying WebAssembly module after the first instantiation @@ -152,73 +88,68 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt constructor(readonly resourceLoader: ResourceLoader, readonly hostInterface: HostInterface) { } private async newModuleInfo(rawModuleId: string, symbolsHint: string, rawModule: RawModule): Promise { - const { flush, manage } = createEmbindPool(); - try { - const rawModuleURL = new URL(rawModule.url); - const symbolsURL = symbolsHint ? resolveSourcePathToURL(symbolsHint, rawModuleURL) : rawModuleURL; - - const instantiateWasmWrapper = - (imports: Emscripten.WebAssemblyImports, - callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { - // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. - return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); - }; - const backend = await createSymbolsBackend({ instantiateWasm: instantiateWasmWrapper }); - const { symbolsFileName, symbolsDwpFileName } = - await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); - const moduleInfo = new ModuleInfo(symbolsURL.href, symbolsFileName, symbolsDwpFileName, backend); - - const addRawModuleResponse = manage(moduleInfo.dwarfSymbolsPlugin.AddRawModule(rawModuleId, symbolsFileName)); - mapVector(manage(addRawModuleResponse.sources), fileName => { - const fileURL = resolveSourcePathToURL(fileName, symbolsURL); - moduleInfo.fileNameToUrl.set(fileName, fileURL.href); - moduleInfo.urlToFileName.set(fileURL.href, fileName); - }); - - for (const dwoFile of mapVector(manage(addRawModuleResponse.dwos), dwo => dwo)) { - const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; - const pathSplit = absolutePath.split('/'); - const fileName = pathSplit.pop() as string; - const parentDirectory = pathSplit.join('/'); - - const dwoURL = new URL(dwoFile, symbolsURL).href; - const dwoResponse = await fetch(dwoURL, { mode: 'no-cors' }) - .catch((e: Error) => ({ ok: false, status: -1, statusText: e.message } as const)); - - if (dwoResponse.ok) { - const dwoData = await dwoResponse.arrayBuffer(); - void this.hostInterface.reportResourceLoad(dwoURL, { success: true, size: dwoData.byteLength }); - - // Sometimes these stick around. - try { - backend.FS.unlink(absolutePath); - } catch { - } - // Ensure directory exists - if (parentDirectory.length > 1) { - // TypeScript doesn't know about createPath - // @ts-expect-error doesn't exit on types - backend.FS.createPath('/', parentDirectory.substring(1), true, true); - } - - backend.FS.createDataFile( - parentDirectory, - fileName, - new Uint8Array(dwoData), - true /* canRead */, - false /* canWrite */, - true /* canOwn */, - ); - } else { - const dwoError = dwoResponse.statusText || `status code ${dwoResponse.status}`; - void this.hostInterface.reportResourceLoad(dwoURL, { success: false, errorMessage: `Failed to fetch dwo file: ${dwoError}` }); + const rawModuleURL = new URL(rawModule.url); + const symbolsURL = symbolsHint ? resolveSourcePathToURL(symbolsHint, rawModuleURL) : rawModuleURL; + + const instantiateWasmWrapper = + (imports: Emscripten.WebAssemblyImports, + callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { + // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. + return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); + }; + const backend = await createSymbolsBackend({ instantiateWasm: instantiateWasmWrapper }); + const { symbolsFileName, symbolsDwpFileName } = + await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); + const moduleInfo = new ModuleInfo(symbolsURL.href, symbolsFileName, symbolsDwpFileName, backend); + + const { sources, dwos } = moduleInfo.dwarfSymbolsPlugin.AddRawModule(rawModuleId, symbolsFileName); + for (const fileName of sources) { + const fileURL = resolveSourcePathToURL(fileName, symbolsURL); + moduleInfo.fileNameToUrl.set(fileName, fileURL.href); + moduleInfo.urlToFileName.set(fileURL.href, fileName); + }; + + for (const dwoFile of dwos) { + const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; + const pathSplit = absolutePath.split('/'); + const fileName = pathSplit.pop() as string; + const parentDirectory = pathSplit.join('/'); + + const dwoURL = new URL(dwoFile, symbolsURL).href; + const dwoResponse = await fetch(dwoURL, { mode: 'no-cors' }) + .catch((e: Error) => ({ ok: false, status: -1, statusText: e.message } as const)); + + if (dwoResponse.ok) { + const dwoData = await dwoResponse.arrayBuffer(); + void this.hostInterface.reportResourceLoad(dwoURL, { success: true, size: dwoData.byteLength }); + + // Sometimes these stick around. + try { + backend.FS.unlink(absolutePath); + } catch { + } + // Ensure directory exists + if (parentDirectory.length > 1) { + // TypeScript doesn't know about createPath + // @ts-expect-error doesn't exit on types + backend.FS.createPath('/', parentDirectory.substring(1), true, true); } - } - return moduleInfo; - } finally { - flush(); + backend.FS.createDataFile( + parentDirectory, + fileName, + new Uint8Array(dwoData), + true /* canRead */, + false /* canWrite */, + true /* canOwn */, + ); + } else { + const dwoError = dwoResponse.statusText || `status code ${dwoResponse.status}`; + void this.hostInterface.reportResourceLoad(dwoURL, { success: false, errorMessage: `Failed to fetch dwo file: ${dwoError}` }); + } } + + return moduleInfo; } async addRawModule(rawModuleId: string, symbolsUrl: string, rawModule: RawModule): Promise { @@ -266,57 +197,39 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt async sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): Promise { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(sourceLocation.rawModuleId); const sourceFile = moduleInfo.urlToFileName.get(sourceLocation.sourceFileURL); if (!sourceFile) { throw new Error(`InternalError: Unknown URL ${sourceLocation.sourceFileURL}`); } - try { - const rawLocations = manage(moduleInfo.dwarfSymbolsPlugin.SourceLocationToRawLocation( - sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber)); - const error = manage(rawLocations.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const { rawModuleId, startOffset, endOffset } = manage(rawLocation); - return { rawModuleId, startOffset, endOffset }; - }); - return locations; - } finally { - flush(); + const { rawLocationRanges, error } = moduleInfo.dwarfSymbolsPlugin.SourceLocationToRawLocation( + sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return rawLocationRanges; } async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): Promise { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); - try { - const sourceLocations = moduleInfo.dwarfSymbolsPlugin.RawLocationToSourceLocation( - rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); - const error = manage(sourceLocations.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const locations = mapVector(manage(sourceLocations.sourceLocation), sourceLocation => { - const sourceFileURL = moduleInfo.fileNameToUrl.get(sourceLocation.sourceFile); - if (!sourceFileURL) { - throw new Error(`InternalError: Unknown source file ${sourceLocation.sourceFile}`); - } - const { rawModuleId, lineNumber, columnNumber } = manage(sourceLocation); - return { - rawModuleId, - sourceFileURL, - lineNumber, - columnNumber, - }; - }); - return locations; - } finally { - flush(); + const { sourceLocation, error } = moduleInfo.dwarfSymbolsPlugin.RawLocationToSourceLocation( + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return sourceLocation.map(({ rawModuleId, sourceFile, lineNumber, columnNumber }) => { + const sourceFileURL = moduleInfo.fileNameToUrl.get(sourceFile); + if (!sourceFileURL) { + throw new Error(`InternalError: Unknown source file ${sourceFile}`); + } + return { + rawModuleId, + sourceFileURL, + lineNumber, + columnNumber, + }; + }); } async getScopeInfo(type: string): Promise { @@ -344,211 +257,104 @@ export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExt } async listVariablesInScope(rawLocation: Chrome.DevTools.RawLocation): Promise { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); - try { - const variables = manage(moduleInfo.dwarfSymbolsPlugin.ListVariablesInScope( - rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0)); - const error = manage(variables.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const apiVariables = mapVector(manage(variables.variable), variable => { - const { scope, name, type } = manage(variable); - return { scope: moduleInfo.stringifyScope(scope), name, type, nestedName: name.split('::') }; - }); - return apiVariables; - } finally { - flush(); + const { variable, error } = moduleInfo.dwarfSymbolsPlugin.ListVariablesInScope( + rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return variable.map(({ scope, name, type }) => { + return { scope, name, type, nestedName: name.split('::') }; + }); } async getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): Promise<{ frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]; }> { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); - try { - const functionInfo = - manage(moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset)); - const error = manage(functionInfo.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const apiFunctionInfos = mapVector(manage(functionInfo.functionNames), functionName => { - return { name: functionName }; - }); - let apiMissingSymbolFiles = mapVector(manage(functionInfo.missingSymbolFiles), x => x); - if (apiMissingSymbolFiles.length && this.resourceLoader.possiblyMissingSymbols) { - apiMissingSymbolFiles = apiMissingSymbolFiles.concat(this.resourceLoader.possiblyMissingSymbols); - } - - return { - frames: apiFunctionInfos, - missingSymbolFiles: apiMissingSymbolFiles.map(x => new URL(x, moduleInfo.symbolsUrl).href) - }; - } finally { - flush(); + const { functionNames, missingSymbolFiles, error } = + moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return { + frames: functionNames.map(name => ({ name })), + missingSymbolFiles: missingSymbolFiles.length > 0 && this.resourceLoader.possiblyMissingSymbols + ? missingSymbolFiles.concat(this.resourceLoader.possiblyMissingSymbols).map(s => new URL(s, moduleInfo.symbolsUrl).href) + : [] + }; } async getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); - try { - const rawLocations = manage( - moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); - const error = manage(rawLocations.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const { rawModuleId, startOffset, endOffset } = manage(rawLocation); - return { rawModuleId, startOffset, endOffset }; - }); - return locations; - } finally { - flush(); + const { rawLocationRanges, error } = + moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return rawLocationRanges; } async getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise { - const { flush, manage } = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); - try { - const rawLocations = manage( - moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); - const error = manage(rawLocations.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { - const { rawModuleId, startOffset, endOffset } = manage(rawLocation); - return { rawModuleId, startOffset, endOffset }; - }); - return locations; - } finally { - flush(); + const { rawLocationRanges, error } = + moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset); + if (error) { + throw new Error(`${error.code}: ${error.message}`); + } + return rawLocationRanges; + } + + async getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { + const moduleInfo = await this.getModuleInfo(rawModuleId); + const sourceFile = moduleInfo.urlToFileName.get(sourceFileURL); + if (!sourceFile) { + throw new Error(`InternalError: Unknown URL ${sourceFileURL}`); + } + const { mappedLines, error } = moduleInfo.dwarfSymbolsPlugin.GetMappedLines(rawModuleId, sourceFile); + if (error) { + throw new Error(`${error.code}: ${error.message}`); } + return mappedLines; } - async getValueInfo(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): Promise<{ - typeInfos: Formatters.TypeInfo[], - root: Formatters.TypeInfo, - location?: number, - data?: number[], - displayValue?: string, - memoryAddress?: number, - } | null> { - const { manage, unmanage, flush } = createEmbindPool(); + async evaluate(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): + Promise { const moduleInfo = await this.getModuleInfo(context.rawModuleId); + const apiRawLocation = new moduleInfo.backend.RawLocation(); try { - const apiRawLocation = manage(new moduleInfo.backend.RawLocation()); apiRawLocation.rawModuleId = context.rawModuleId; apiRawLocation.codeOffset = context.codeOffset; apiRawLocation.inlineFrameIndex = context.inlineFrameIndex || 0; const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); - const proxy = new Formatters.DebuggerProxy(wasm, moduleInfo.backend); - const typeInfoResult = - manage(moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, proxy)); - const error = manage(typeInfoResult.error); + const debuggerProxy = new Formatters.DebuggerProxy(wasm, moduleInfo.backend); + + const { typeInfos, root, displayValue, location, memoryAddress, data, error } = + moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, debuggerProxy); + if (error) { - if (error.code === moduleInfo.backend.ErrorCode.MODULE_NOT_FOUND_ERROR) { + if (error.code === 'MODULE_NOT_FOUND_ERROR') { // Let's not throw when the module gets unloaded - that is quite common path that // we hit when the source-scope pane still keeps asynchronously updating while we // unload the wasm module. - return null; + return { + type: 'undefined' as Chrome.DevTools.RemoteObjectType, + hasChildren: false, + description: '', + }; } // TODO(crbug.com/1271147) Instead of throwing, we whould create an AST error node with the message // so that it is properly surfaced to the user. This should then make the special handling of // MODULE_NOT_FOUND_ERROR unnecessary. - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - - const typeInfos = mapVector(manage(typeInfoResult.typeInfos), typeInfo => fromApiTypeInfo(manage(typeInfo))); - const root = fromApiTypeInfo(manage(typeInfoResult.root)); - const { location, displayValue, memoryAddress } = typeInfoResult; - const data = typeInfoResult.data ? mapVector(manage(typeInfoResult.data), n => n) : undefined; - return { typeInfos, root, location, data, displayValue, memoryAddress }; - - function fromApiTypeInfo(apiTypeInfo: SymbolsBackend.TypeInfo): Formatters.TypeInfo { - const apiMembers = manage(apiTypeInfo.members); - const members = mapVector(apiMembers, fieldInfo => mapFieldInfo(manage(fieldInfo))); - const apiEnumerators = manage(apiTypeInfo.enumerators); - const enumerators = mapVector(apiEnumerators, enumerator => mapEnumerator(manage(enumerator))); - unmanage(apiEnumerators); - const typeNames = mapVector(manage(apiTypeInfo.typeNames), e => e); - unmanage(apiMembers); - const { typeId, size, arraySize, alignment, canExpand, isPointer, hasValue } = apiTypeInfo; - const formatter = Formatters.CustomFormatters.get({ - typeNames, - typeId, - size, - alignment, - isPointer, - canExpand, - arraySize: arraySize ?? 0, - hasValue, - members, - enumerators, - }); - return { - typeNames, - isPointer, - typeId, - size, - alignment, - canExpand: canExpand && !formatter, - arraySize: arraySize ?? 0, - hasValue: hasValue || Boolean(formatter), - members, - enumerators, - }; + throw new Error(`${error.code}: ${error.message}`); } - } finally { - flush(); - } - } - - async getMappedLines(rawModuleId: string, sourceFileURL: string): Promise { - const { flush, manage } = createEmbindPool(); - const moduleInfo = await this.getModuleInfo(rawModuleId); - const sourceFile = moduleInfo.urlToFileName.get(sourceFileURL); - if (!sourceFile) { - throw new Error(`InternalError: Unknown URL ${sourceFileURL}`); - } - try { - const mappedLines = manage(moduleInfo.dwarfSymbolsPlugin.GetMappedLines(rawModuleId, sourceFile)); - const error = manage(mappedLines.error); - if (error) { - throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); - } - const lines = mapVector(manage(mappedLines.MappedLines), l => l); - return lines; + const value = { typeInfos, root, location, data, displayValue, memoryAddress }; + return Formatters.CXXValue.create(this.lazyObjects, wasm, wasm.view, value).asRemoteObject(); } finally { - flush(); - } - } - - async evaluate(expression: string, context: SymbolsBackend.RawLocation, stopId: unknown): - Promise { - const valueInfo = await this.getValueInfo(expression, context, stopId); - if (!valueInfo) { - return null; - } - - const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); - const cxxObject = await Formatters.CXXValue.create(this.lazyObjects, wasm, wasm.view, valueInfo); - if (!cxxObject) { - return { - type: 'undefined' as Chrome.DevTools.RemoteObjectType, - hasChildren: false, - description: '', - }; + apiRawLocation.delete(); } - return await cxxObject.asRemoteObject(); } async getProperties(objectId: Chrome.DevTools.RemoteObjectId): Promise { diff --git a/src/Formatters.ts b/src/Formatters.ts index 74a7aca..08b5587 100644 --- a/src/Formatters.ts +++ b/src/Formatters.ts @@ -5,11 +5,12 @@ import { CustomFormatters, + type FieldInfo, type LazyObject, PrimitiveLazyObject, type TypeInfo, type Value, - type WasmInterface, + type WasmInterface } from './CustomFormatters'; import type { ForeignObject } from './WasmTypes'; @@ -43,6 +44,9 @@ CustomFormatters.addFormatter({ types: ['uint8_t', 'int8_t'], format: formatChar export function formatChar(wasm: WasmInterface, value: Value): string | number { const char = value.typeNames.includes('int8_t') ? Math.abs(value.asInt8()) : value.asUint8(); + if (value.isEnumeratedValue) { + return char; + } switch (char) { case 0x0: return '\'\\0\''; @@ -299,3 +303,150 @@ export function formatExternRef(wasm: WasmInterface, value: Value): () => LazyOb return () => obj; } CustomFormatters.addFormatter({ types: ['__externref_t', 'externref_t'], format: formatExternRef }); + +/* + * Rust language support + */ +export const enum LanguageId { + Rust = 0x1c, +} + +type TypeInfoWithExtendedInfo = TypeInfo & Required>; + +function extendedLanguageTypeMatch( + languageId: LanguageId, + predicate: string | RegExp | ((type: TypeInfoWithExtendedInfo) => boolean), +): (type: TypeInfo) => boolean { + if (typeof predicate !== 'function') { + if (typeof predicate === 'string') { + const typeName = predicate; + predicate = type => type.typeNames.includes(typeName); + } else { + const typeRegExp = predicate; + predicate = type => type.typeNames.some(typeName => typeRegExp.test(typeName)); + } + } + return type => { + return type.extendedInfo?.languageId === languageId && predicate(type as TypeInfoWithExtendedInfo); + }; +} + +const rustTypeMatch = extendedLanguageTypeMatch.bind(null, LanguageId.Rust); + +function hasOnlyRustTupleLikeMembers(type: TypeInfo) { + return type.members.length > 0 && type.members.every(({ name }, i) => name === `__${i}`); +} + +function hasVariantParts(type: TypeInfo) { + return type.extendedInfo !== undefined && type.extendedInfo.variantParts.length > 0; +} + +function unwrapLoneRustTupleLikeMember(value: Value) { + const members = value.getMembers(); + return members.length === 1 && members[0] === '__0' ? value.$('__0') : value; +} + +export function formatRustSumType(wasm: WasmInterface, value: Value) { + const variantPart = value.extendedTypeInfo!.variantParts[0]; + if (!variantPart) { + throw new Error(`Can't format sum type without variant info`); + } + + const discriminatorValue = getDiscriminatorValue(value, variantPart.discriminatorMember); + const variant = + variantPart.variants.find(v => v.discriminatorValue === discriminatorValue) || + variantPart.variants.find(v => v.discriminatorValue === undefined); + if (!variant) { + throw new Error(`Invalid discriminator value ${discriminatorValue}`); + } + if (!variant.members[0]) { + throw new Error(`Can't format sum type where variant is missing member info`); + } + + const { name = discriminatorValue.toString(), typeId, offset } = variant.members[0]; + const variantValue = value.asType(typeId, offset); + + return { [name]: unwrapLoneRustTupleLikeMember(variantValue) }; +} + +export function formatRustTuple(wasm: WasmInterface, value: Value): Value[] | { [key: string]: Value; } { + return value.getMembers().map(m => value.$(m)); +} + +export function formatRustArraySlice(wasm: WasmInterface, value: Value) { + const data = value.$('data_ptr'); + const size = value.$('length').asUint32(); + const elements: Value[] = []; + for (let i = 0; i < size; ++i) { + elements.push(data.$(i)); + } + return elements; +} + +export function formatRustVector(wasm: WasmInterface, value: Value) { + const elementTypeId = value.extendedTypeInfo?.templateParameters[0]?.typeId; + if (!elementTypeId) { + throw new Error(`Can't determine element type`); + } + const data = value.$('buf.inner.ptr').asPointerToType(elementTypeId); + const size = value.$('len').asUint32(); + const elements: Value[] = []; + for (let i = 0; i < size; ++i) { + elements.push(data.$(i)); + } + return elements; +} + +export function formatRustStringSlice(wasm: WasmInterface, value: Value) { + const data = value.$('data_ptr').asUint32(); + const size = value.$('length').asUint32(); + const bytes = wasm.readMemory(data, Math.min(size, Constants.MAX_STRING_LEN)); + const decoder = new TextDecoder(); + return formattedStringResult(bytes, decoder.decode.bind(decoder)); +} + +export function formatRustString(wasm: WasmInterface, value: Value) { + const data = value.$('vec.buf.inner.ptr').asUint32(); + const size = value.$('vec.len').asUint32(); + const bytes = wasm.readMemory(data, Math.min(size, Constants.MAX_STRING_LEN)); + const decoder = new TextDecoder(); + return formattedStringResult(bytes, decoder.decode.bind(decoder)); +} + +export function formatRustRcPointer(wasm: WasmInterface, value: Value): { [key: string]: Value | null | number; } { + const box = value.$('ptr.pointer.*'); + const boxedValue = box.$('value'); + const strongCount = box.$('strong').asUint32(); + const weakCount = box.$('weak').asUint32() - 1; // Rc::weak_count() substracts one + if (!strongCount) { + return { '0x0': null }; + } + return { + [`0x${boxedValue.location.toString(16)}`]: boxedValue, + ['strong_count']: strongCount, + ['weak_count']: weakCount, + }; +} + +function getDiscriminatorValue(value: Value, discriminatorMember: FieldInfo) { + const discriminatorValue = value.asType(discriminatorMember.typeId, discriminatorMember.offset); + switch (discriminatorValue.size) { + case 1: + return BigInt(discriminatorValue.asUint8()); + case 2: + return BigInt(discriminatorValue.asUint16()); + case 4: + return BigInt(discriminatorValue.asUint32()); + case 8: + return discriminatorValue.asUint64(); + } + throw new Error(`Invalid size ${discriminatorValue.size} from discriminator member`); +} + +CustomFormatters.addFormatter({ types: rustTypeMatch(hasVariantParts), format: formatRustSumType }); +CustomFormatters.addFormatter({ types: rustTypeMatch(hasOnlyRustTupleLikeMembers), format: formatRustTuple }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^&\[.+\]$/), format: formatRustArraySlice }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^alloc::vec::Vec<.+>$/), format: formatRustVector }); +CustomFormatters.addFormatter({ types: rustTypeMatch('&str'), format: formatRustStringSlice }); +CustomFormatters.addFormatter({ types: rustTypeMatch('alloc::string::String'), format: formatRustString }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^alloc::rc::(Rc|Weak)<.+>$/), format: formatRustRcPointer }); diff --git a/src/SymbolsBackend.d.ts b/src/SymbolsBackend.d.ts index 329435d..4ff1dd8 100644 --- a/src/SymbolsBackend.d.ts +++ b/src/SymbolsBackend.d.ts @@ -5,18 +5,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ -export interface EmbindObject { - new(): this; - delete(): void; -} - -export interface Vector extends EmbindObject { - size(): number; - get(index: number): T; - push_back(value: T): void; -} +import { EmbindObject, EnumDefinition, EnumValue, Vector } from './embind'; -export interface ErrorCode extends EmbindObject { } +export interface ErrorCode extends EnumValue< + ErrorCode, + | 'INTERNAL_ERROR' + | 'PROTOCOL_ERROR' + | 'MODULE_NOT_FOUND_ERROR' + | 'TYPE_NAME_IN_MODULE_NOT_FOUND_ERROR' + | 'EVAL_ERROR'> { } export interface Error extends EmbindObject { code: ErrorCode; @@ -42,7 +39,11 @@ export interface SourceLocation extends EmbindObject { columnNumber: number; } -export interface VariableScope extends EmbindObject { } +export interface VariableScope extends EnumValue< + VariableScope, + | 'LOCAL' + | 'PARAMETER' + | 'GLOBAL'> { } export interface Variable extends EmbindObject { scope: VariableScope; @@ -63,6 +64,27 @@ export interface Enumerator extends EmbindObject { typeId: string; } +export interface VariantInfo extends EmbindObject { + discriminatorValue: bigint | undefined; + members: Vector; +} + +export interface VariantPartInfo extends EmbindObject { + discriminatorMember: FieldInfo; + variants: Vector; +} + +export interface TemplateParameterInfo extends EmbindObject { + typeId: string; + name: string | undefined; +} + +export interface ExtendedTypeInfo extends EmbindObject { + languageId: number; + variantParts: Vector; + templateParameters: Vector; +} + export interface TypeInfo extends EmbindObject { typeNames: Vector; typeId: string; @@ -70,10 +92,11 @@ export interface TypeInfo extends EmbindObject { size: number; canExpand: boolean; hasValue: boolean; - arraySize: number | undefined; + arraySize: number; isPointer: boolean; members: Vector; enumerators: Vector; + extendedInfo: ExtendedTypeInfo | undefined; } export interface AddRawModuleResponse extends EmbindObject { @@ -114,7 +137,7 @@ export interface GetInlinedCalleesRangesResponse extends EmbindObject { } export interface GetMappedLinesResponse extends EmbindObject { - MappedLines: Vector; + mappedLines: Vector; error: Error | undefined; } @@ -154,13 +177,15 @@ export interface Module extends EmscriptenModule { TypeInfoArray: Vector; FieldInfoArray: Vector; EnumeratorArray: Vector; - ErrorCode: - {INTERNAL_ERROR: ErrorCode, PROTOCOL_ERROR: ErrorCode, MODULE_NOT_FOUND_ERROR: ErrorCode, EVAL_ERROR: ErrorCode}; + VariantPartInfoArray: Vector; + VariantInfoArray: Vector; + TemplateParameterInfoArray: Vector; + ErrorCode: EnumDefinition; Error: Error; RawLocationRange: RawLocationRange; RawLocation: RawLocation; SourceLocation: SourceLocation; - VariableScope: {LOCAL: VariableScope, PARAMETER: VariableScope, GLOBAL: VariableScope}; + VariableScope: EnumDefinition; Variable: Variable; FieldInfo: FieldInfo; Enumerator: Enumerator; diff --git a/src/embind.ts b/src/embind.ts new file mode 100644 index 0000000..4e94fbc --- /dev/null +++ b/src/embind.ts @@ -0,0 +1,113 @@ + +interface HasClassPointer { + ['$$']: { '__embind_type_marker': T; }; +}; + +export interface EmbindObject extends HasClassPointer { + new(): this; + delete(): void; +} + +export interface Vector extends EmbindObject> { + size(): number; + get(index: number): T; + push_back(value: T): void; +} + +export interface EnumValue extends EmbindObject { + value(): number; +} + +export type EnumDefinition> = T['$$']['__embind_type_marker']['values']; + +export type UnmarshalObject = + T extends EnumValue ? E : + T extends Vector ? UnmarshalObject[] : + T extends EmbindObject ? { [P in Exclude]: UnmarshalObject } : + T; + +export type UnmarshalInterface = { + [P in Exclude>]: T[P] extends (...args: infer A) => infer R + ? (...args: A) => UnmarshalObject + : never +}; + +export interface UnmarshallerOptions { + enumTypes?: Record[]; +} + +export interface UnmarshalledInterfaceOptions extends UnmarshallerOptions { + methods: K[]; +} + +function prototypeOfHasMethods(obj: unknown, ...methods: string[]): boolean { + return typeof obj === 'object' && obj !== null && methods.every(m => m in Object.getPrototypeOf(obj)); +} + +function isEmbindObject(obj: unknown): obj is EmbindObject { + return prototypeOfHasMethods(obj, 'delete'); +} + +function isEmVector(obj: unknown): obj is Vector { + return prototypeOfHasMethods(obj, 'delete', 'size', 'get', 'push_back'); +} + +export function getUnmarshaller(options: UnmarshallerOptions) { + const { enumTypes = [] } = options; + + const enumValueToNameMap = new Map( + enumTypes.flatMap(t => + Object.entries(t) + .filter(([, value]) => 'value' in value) + .map(([name, value]) => [value, name])) + ); + + return function unmarshal(obj: (T & EmbindObject) | UnmarshalObject): UnmarshalObject { + + if (typeof obj === 'object' && obj !== null && enumValueToNameMap.has(obj)) { + return enumValueToNameMap.get(obj) as UnmarshalObject & string; + } + + if (!isEmbindObject(obj)) { + return obj; + } + + try { + if (isEmVector(obj)) { + const unmarshalledArray: unknown[] = []; + unmarshalledArray.length = obj.size(); + for (let i = 0; i < unmarshalledArray.length; i++) { + unmarshalledArray[i] = unmarshal(obj.get(i)); + } + return unmarshalledArray as UnmarshalObject; + } else { + const unmarshalledObj: Record = {}; + for (const k in obj) { + const v = (obj as Record)[k]; + if (typeof v !== 'function') { + unmarshalledObj[k] = unmarshal(v); + } + } + return unmarshalledObj as UnmarshalObject; + } + } finally { + obj.delete(); + } + }; +} + +export function getUnmarshalledInterface any }, K extends keyof T>( + target: T, + options: UnmarshalledInterfaceOptions, +): UnmarshalInterface> { + + const { methods } = options; + const unmarshal = getUnmarshaller(options); + + return Object.fromEntries(methods.map(method => [ + method, + (...args: any[]) => { + return unmarshal(target[method](...args)); + } + ])) as UnmarshalInterface>; +} diff --git a/test-utils/SymbolsBackendPlugin.ts b/test-utils/SymbolsBackendPlugin.ts new file mode 100644 index 0000000..80a35a6 --- /dev/null +++ b/test-utils/SymbolsBackendPlugin.ts @@ -0,0 +1,85 @@ + +import { basename } from 'path'; +import createSymbolsBackend, { RawLocation, Module as SymbolsBackendModule } from '../dist/SymbolsBackend'; +import { DebuggerProxy, WasmMemoryView } from '../src/CustomFormatters'; +import { getUnmarshalledInterface, UnmarshalObject } from '../src/embind'; +import { createSyncInterface, SyncInterface } from './sync-interface'; +import TestWasmInterface from './TestWasmInterface'; + +export default class SymbolsBackendPlugin { + + static create(...preLoadedFiles: string[]): ReturnType['__create']> { + // createSyncInterface wrapped instances run in it's own worker thread so we can + // be sure not to pollute our global context by loading the EmScripten module. + const worker = createSyncInterface(__filename, '__create', preLoadedFiles); + return { + AddRawModule: (...args) => worker.invoke('AddRawModule', ...args), + RemoveRawModule: (...args) => worker.invoke('RemoveRawModule', ...args), + SourceLocationToRawLocation: (...args) => worker.invoke('SourceLocationToRawLocation', ...args), + RawLocationToSourceLocation: (...args) => worker.invoke('RawLocationToSourceLocation', ...args), + ListVariablesInScope: (...args) => worker.invoke('ListVariablesInScope', ...args), + GetFunctionInfo: (...args) => worker.invoke('GetFunctionInfo', ...args), + GetInlinedFunctionRanges: (...args) => worker.invoke('GetInlinedFunctionRanges', ...args), + GetInlinedCalleesRanges: (...args) => worker.invoke('GetInlinedCalleesRanges', ...args), + GetMappedLines: (...args) => worker.invoke('GetMappedLines', ...args), + EvaluateExpression: (...args) => worker.invoke('EvaluateExpression', ...args), + delete: () => { + worker.invoke('delete'); + worker.dispose(); + } + }; + } + + static async __create(preLoadedFiles: string[]) { + + require('./emscripten-module-runner/prelude'); + + const module = await createSymbolsBackend({ + // @ts-expect-error + preRun({ FS }: SymbolsBackendModule) { + for (const path of preLoadedFiles) { + FS.createPreloadedFile('', basename(path), path, true, false); + } + } + }); + + const instance = new module.DWARFSymbolsPlugin(); + + const unmarshalledInteface = getUnmarshalledInterface(instance, { + methods: [ + 'AddRawModule', + 'RemoveRawModule', + 'SourceLocationToRawLocation', + 'RawLocationToSourceLocation', + 'ListVariablesInScope', + 'GetFunctionInfo', + 'GetInlinedFunctionRanges', + 'GetInlinedCalleesRanges', + 'GetMappedLines', + 'EvaluateExpression', + 'delete' + ], + enumTypes: [ + module.ErrorCode, + module.VariableScope] + }); + + return { + ...unmarshalledInteface, + EvaluateExpression(location: UnmarshalObject, expression: string, debugProxy: TestWasmInterface) { + const locationMarshalled = new module.RawLocation(); + Object.assign(locationMarshalled, location); + + const wasm = new TestWasmInterface(); + const debugProxyMarshalled = new DebuggerProxy(wasm, module); + Object.assign(wasm, debugProxy, { view: new WasmMemoryView(wasm) }); + + try { + return unmarshalledInteface.EvaluateExpression(locationMarshalled, expression, debugProxyMarshalled); + } finally { + locationMarshalled.delete(); + } + } + }; + } +} \ No newline at end of file diff --git a/test-utils/TestValue.ts b/test-utils/TestValue.ts index d09d087..1c112d2 100644 --- a/test-utils/TestValue.ts +++ b/test-utils/TestValue.ts @@ -3,14 +3,24 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/TestUtils.ts -import type { Value } from '../src/CustomFormatters'; +import type { ExtendedTypeInfo, Value } from '../src/CustomFormatters'; + +interface TypedArrayLike { + readonly BYTES_PER_ELEMENT: number; + readonly buffer: ArrayBufferLike; + readonly byteLength: number; + readonly byteOffset: number; +} export default class TestValue implements Value { private dataView: DataView; + private typeMap: { [typeId: string]: TestValue; } = {}; members: { [key: string]: TestValue, [key: number]: TestValue; }; location: number; size: number; typeNames: string[]; + isEnumeratedValue: boolean = false; + extendedTypeInfo?: ExtendedTypeInfo | undefined; static fromInt8(value: number, typeName = 'int8_t'): TestValue { const content = new DataView(new ArrayBuffer(1)); @@ -72,8 +82,10 @@ export default class TestValue implements Value { for (let i = 0; i < elements.length; ++i) { members[i] = elements[i]; } - const value = new TestValue(content, `${elements[0].typeNames[0]}${space}*`, members); - return value; + return new TestValue(content, `${elements[0].typeNames[0]}${space}*`, members); + } + static fromType(typeName: string, data: TypedArrayLike = new Uint8Array(), location?: number) { + return new TestValue(new DataView(data.buffer), typeName, undefined, location); } static fromMembers(typeName: string, members: { [key: string]: TestValue, [key: number]: TestValue; }): TestValue { return new TestValue(new DataView(new ArrayBuffer(0)), typeName, members); @@ -109,6 +121,18 @@ export default class TestValue implements Value { asFloat64(): number { return this.dataView.getFloat64(0, true); } + asType(typeId: string, offset = 0): Value { + return this.typeMap[typeId] ?? new TestValue(this.dataView, typeId, undefined, this.location + offset); + } + asPointerToType(typeId: string): Value { + return new TestValue(this.dataView, `${typeId} *`, new Proxy({}, { + get(_target, p) { + if (typeof p === 'string' && /^\*|(\d+)$/.test(p)) { + return TestValue.fromType(typeId); + } + } + })); + } asDataView(offset?: number, size?: number): DataView { offset = this.location + (offset ?? 0); size = Math.min(size ?? this.size, this.size - Math.max(0, offset)); @@ -117,25 +141,59 @@ export default class TestValue implements Value { getMembers(): string[] { return Object.keys(this.members); } - $(member: string | number): Value { if (typeof member === 'number' || !member.includes('.')) { - return this.members[member]; + return this.members[member] || (() => { throw new Error(`Invalid member ${member}`); })(); } - let value = this as Value; + let value: Value = this; for (const prop of member.split('.')) { value = value.$(prop); } return value; } + offsetBy(offset: number) { + return new TestValue(this.dataView, this.typeNames[0], this.members, this.location + offset); + } + + registerAsType(value: TestValue): string { + const typeId = `${Object.keys(this.typeMap).length + 1}`; + this.typeMap[typeId] = value; + return typeId; + } + + withVariants(discriminator: TestValue, ...variants: [number, string, TestValue][]) { + this.extendedTypeInfo ??= { languageId: 0, variantParts: [], templateParameters: [] }; + this.extendedTypeInfo.variantParts = [{ + discriminatorMember: { typeId: this.registerAsType(discriminator), offset: 0 }, + variants: variants.map(([discriminatorValue, name, variant]) => ({ + discriminatorValue: BigInt(discriminatorValue), + members: [{ + name, + typeId: this.registerAsType(TestValue.fromMembers(name, { '__0': variant.offsetBy(discriminator.size) })), + offset: discriminator.size, + }] + })), + }]; + return this; + } + + withTemplateParameters(...typeNames: string[]) { + this.extendedTypeInfo ??= { languageId: 0, variantParts: [], templateParameters: [] }; + this.extendedTypeInfo.templateParameters.push(...typeNames.map(typeId => ({ typeId }))); + return this; + } + constructor( - content: DataView, typeName: string, - members?: { [key: string]: TestValue, [key: number]: TestValue; }) { - this.location = 0; + content: DataView, + typeName: string, + members: { [key: string]: TestValue, [key: number]: TestValue; } = {}, + location: number = 0, + ) { + this.location = location; this.size = content.byteLength; this.typeNames = [typeName]; - this.members = members || {}; + this.members = members; this.dataView = content; } } diff --git a/test-utils/TestWasmInterface.ts b/test-utils/TestWasmInterface.ts index 64d66b6..6b23faf 100644 --- a/test-utils/TestWasmInterface.ts +++ b/test-utils/TestWasmInterface.ts @@ -3,7 +3,7 @@ // // https://github.com/ChromeDevTools/devtools-frontend/blob/main/extensions/cxx_debugging/tests/TestUtils.ts -import type { WasmInterface } from '../src/CustomFormatters'; +import { WasmMemoryView, type WasmInterface } from '../src/CustomFormatters'; import type { WasmValue } from '../src/WasmTypes'; export default class TestWasmInterface implements WasmInterface { @@ -11,10 +11,20 @@ export default class TestWasmInterface implements WasmInterface { locals = new Map(); globals = new Map(); stack = new Map(); + view = new WasmMemoryView(this); readMemory(offset: number, length: number): Uint8Array { return new Uint8Array(this.memory, offset, length); } + writeMemory(offset: number, data: { buffer: ArrayBufferLike; }) { + let memory = new Uint8Array(this.memory); + if (offset + data.buffer.byteLength > memory.buffer.byteLength) { + memory = new Uint8Array(new ArrayBuffer(offset + data.buffer.byteLength)); + memory.set(new Uint8Array(this.memory)); + this.memory = memory.buffer; + } + memory.set(new Uint8Array(data.buffer), offset); + } getOp(op: number): WasmValue { const val = this.stack.get(op); if (val !== undefined) { diff --git a/test-utils/emscripten-module-runner/child-process.js b/test-utils/emscripten-module-runner/child-process.js index 2f42f8e..8dc710f 100644 --- a/test-utils/emscripten-module-runner/child-process.js +++ b/test-utils/emscripten-module-runner/child-process.js @@ -2,7 +2,6 @@ const [moduleImportPath, callbackString] = process.argv.splice(2, 4); const moduleImports = require(moduleImportPath); const callback = eval(callbackString); -// See: https://github.com/emscripten-core/emscripten/issues/16742 -global['Browser'] = { handledByPreloadPlugin: () => false }; +require('./prelude'); -callback(moduleImports); \ No newline at end of file +callback(moduleImports); diff --git a/test-utils/emscripten-module-runner/prelude.js b/test-utils/emscripten-module-runner/prelude.js new file mode 100644 index 0000000..7dec54f --- /dev/null +++ b/test-utils/emscripten-module-runner/prelude.js @@ -0,0 +1,2 @@ +// See: https://github.com/emscripten-core/emscripten/issues/16742 +global['Browser'] = { handledByPreloadPlugin: () => false }; diff --git a/test/Formatters.test.ts b/test/Formatters.test.ts index 6d516bc..d0686f2 100644 --- a/test/Formatters.test.ts +++ b/test/Formatters.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { describe, it } from 'node:test'; -import { CustomFormatters, type TypeInfo } from '../src/CustomFormatters'; +import { CustomFormatters, ExtendedTypeInfo, type TypeInfo } from '../src/CustomFormatters'; import * as Formatters from '../src/Formatters'; import TestValue from '../test-utils/TestValue'; import TestWasmInterface from '../test-utils/TestWasmInterface'; @@ -248,6 +248,159 @@ describe('Formatters', () => { linearMemoryAddress: undefined, }); }); + + describe('Rust', () => { + + it('formatRustSumType', () => { + const wasm = new TestWasmInterface(); + const discriminator = new Uint16Array(1); + const value = TestValue.fromType('test::Animal') + .withVariants( + TestValue.fromType('u16', discriminator), + [1, 'Dog', TestValue.fromType('test::Animal::Dog')], + [2, 'Cat', TestValue.fromType('test::Animal::Cat')]); + + discriminator[0] = 1; + assert.deepEqual( + Formatters.formatRustSumType(wasm, value), + { 'Dog': TestValue.fromType('test::Animal::Dog', undefined, 2) } + ); + + discriminator[0] = 2; + assert.deepEqual( + Formatters.formatRustSumType(wasm, value), + { 'Cat': TestValue.fromType('test::Animal::Cat', undefined, 2) } + ); + }); + + it('formatRustTuple', () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('(i32, f32)', { + __0: TestValue.fromInt32(2), + __1: TestValue.fromFloat32(3.5) + }); + + assert.deepEqual( + Formatters.formatRustTuple(wasm, value), + [ + TestValue.fromInt32(2), + TestValue.fromFloat32(3.5), + ] + ); + }); + + it('formatRustArraySlice', async () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('&[i32]', { + length: TestValue.fromInt32(3), + data_ptr: TestValue.pointerTo([ + TestValue.fromType('i32', new Uint32Array([1])), + TestValue.fromType('i32', new Uint32Array([2])), + TestValue.fromType('i32', new Uint32Array([3])), + ]) + }); + + assert.deepEqual( + Formatters.formatRustArraySlice(wasm, value), + [ + TestValue.fromType('i32', new Uint32Array([1])), + TestValue.fromType('i32', new Uint32Array([2])), + TestValue.fromType('i32', new Uint32Array([3])), + ] + ); + }); + + it('formatRustVector', async () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('alloc::vec::Vec', { + len: TestValue.fromInt32(3), + buf: TestValue.fromMembers('_', { + inner: TestValue.fromMembers('_', { + ptr: TestValue.fromUint32(0, 'i32') + }) + }) + }).withTemplateParameters('test::Type'); + + assert.deepEqual( + Formatters.formatRustVector(wasm, value), + [ + TestValue.fromType('test::Type'), + TestValue.fromType('test::Type'), + TestValue.fromType('test::Type'), + ] + ); + }); + + it('formatRustStringSlice', () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('&str', { + length: TestValue.fromInt32(6), + 'data_ptr': TestValue.fromUint32(10, 'unsigned char *') + }); + + const chars = new TextEncoder().encode('Hello World'); + wasm.writeMemory(8, chars); + + assert.deepEqual( + Formatters.formatRustStringSlice(wasm, value), + { + size: 6, + string: '"llo Wo"', + chars: chars.slice(2, 8), + } + ); + }); + + it('formatRustString', () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('alloc::string::String', { + vec: TestValue.fromMembers('_', { + len: TestValue.fromInt32(11), + buf: TestValue.fromMembers('_', { + inner: TestValue.fromMembers('_', { + ptr: TestValue.fromUint32(8, 'unsigned char *') + }) + }) + }) + }); + + const chars = new TextEncoder().encode('Hello World'); + wasm.writeMemory(8, chars); + + assert.deepEqual( + Formatters.formatRustString(wasm, value), + { + size: 11, + string: '"Hello World"', + chars, + } + ); + }); + + it('formatRustRcPointer', () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('alloc::rc::Rc', { + ptr: TestValue.fromMembers('_', { + pointer: TestValue.pointerTo( + TestValue.fromMembers('_ *', { + value: TestValue.fromType('test::Type', undefined, 0x1234), + strong: TestValue.fromUint32(1), + weak: TestValue.fromUint32(3), + }), + ), + }) + }); + + assert.deepEqual( + Formatters.formatRustRcPointer(wasm, value), + { + '0x1234': TestValue.fromType('test::Type', undefined, 0x1234), + strong_count: 1, + weak_count: 2, + } + ); + }); + }); }); describe('CustomFormatters', () => { @@ -260,71 +413,125 @@ describe('CustomFormatters', () => { return { typeNames: [typeName], typeId: typeName, - members: [], alignment: 0, - arraySize: 0, size: 0, - isPointer: Boolean(typeName.match(/^.+\*$/) || typeName.match(/^.+&$/)), canExpand: false, hasValue: false, + arraySize: 0, + isPointer: Boolean(typeName.match(/^.+\*$/) || typeName.match(/^.+&$/)), + members: [], + enumerators: [], ...typeProperties, }; }; - assert.deepEqual(CustomFormatters.get(type('std::__2::string'))?.format, Formatters.formatLibCXX8String); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('std::__2::string'))?.format, Formatters.formatLibCXX8String); + assert.strictEqual( CustomFormatters .get(type('std::__2::basic_string, std::__2::allocator >')) ?.format, Formatters.formatLibCXX8String); - assert.deepEqual(CustomFormatters.get(type('std::__2::u8string'))?.format, Formatters.formatLibCXX8String); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('std::__2::u8string'))?.format, Formatters.formatLibCXX8String); + assert.strictEqual( CustomFormatters .get(type( 'std::__2::basic_string, std::__2::allocator >')) ?.format, Formatters.formatLibCXX8String); - assert.deepEqual(CustomFormatters.get(type('std::__2::u16string'))?.format, Formatters.formatLibCXX16String); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('std::__2::u16string'))?.format, Formatters.formatLibCXX16String); + assert.strictEqual( CustomFormatters .get(type( 'std::__2::basic_string, std::__2::allocator >')) ?.format, Formatters.formatLibCXX16String); - assert.deepEqual(CustomFormatters.get(type('std::__2::wstring'))?.format, Formatters.formatLibCXX32String); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('std::__2::wstring'))?.format, Formatters.formatLibCXX32String); + assert.strictEqual( CustomFormatters .get(type( 'std::__2::basic_string, std::__2::allocator >')) ?.format, Formatters.formatLibCXX32String); - assert.deepEqual(CustomFormatters.get(type('std::__2::u32string'))?.format, Formatters.formatLibCXX32String); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('std::__2::u32string'))?.format, Formatters.formatLibCXX32String); + assert.strictEqual( CustomFormatters .get(type( 'std::__2::basic_string, std::__2::allocator >')) ?.format, Formatters.formatLibCXX32String); - assert.deepEqual(CustomFormatters.get(type('char *'))?.format, Formatters.formatCString); - assert.deepEqual(CustomFormatters.get(type('char8_t *'))?.format, Formatters.formatCString); - assert.deepEqual(CustomFormatters.get(type('char16_t *'))?.format, Formatters.formatU16CString); - assert.deepEqual(CustomFormatters.get(type('wchar_t *'))?.format, Formatters.formatCWString); - assert.deepEqual(CustomFormatters.get(type('char32_t *'))?.format, Formatters.formatCWString); - assert.deepEqual( + assert.strictEqual(CustomFormatters.get(type('char *'))?.format, Formatters.formatCString); + assert.strictEqual(CustomFormatters.get(type('char8_t *'))?.format, Formatters.formatCString); + assert.strictEqual(CustomFormatters.get(type('char16_t *'))?.format, Formatters.formatU16CString); + assert.strictEqual(CustomFormatters.get(type('wchar_t *'))?.format, Formatters.formatCWString); + assert.strictEqual(CustomFormatters.get(type('char32_t *'))?.format, Formatters.formatCWString); + assert.strictEqual( CustomFormatters.get(type('int (*)()', { isPointer: true }))?.format, Formatters.formatPointerOrReference); - assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); - assert.deepEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); + assert.strictEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); + assert.strictEqual(CustomFormatters.get(type('std::vector'))?.format, Formatters.formatVector); - assert.deepEqual(CustomFormatters.get(type('int *'))?.format, Formatters.formatPointerOrReference); - assert.deepEqual(CustomFormatters.get(type('int &'))?.format, Formatters.formatPointerOrReference); - assert.deepEqual(CustomFormatters.get(type('int[]'))?.format, Formatters.formatDynamicArray); + assert.strictEqual(CustomFormatters.get(type('int *'))?.format, Formatters.formatPointerOrReference); + assert.strictEqual(CustomFormatters.get(type('int &'))?.format, Formatters.formatPointerOrReference); + assert.strictEqual(CustomFormatters.get(type('int[]'))?.format, Formatters.formatDynamicArray); - assert.deepEqual(CustomFormatters.get(type('unsigned __int128'))?.format, Formatters.formatUInt128); - assert.deepEqual(CustomFormatters.get(type('__int128'))?.format, Formatters.formatInt128); + assert.strictEqual(CustomFormatters.get(type('unsigned __int128'))?.format, Formatters.formatUInt128); + assert.strictEqual(CustomFormatters.get(type('__int128'))?.format, Formatters.formatInt128); }); } + + it(`looks up formatters correctly for rust types`, () => { + const type = (typeName: string, typeProperties: Partial = {}, extendedInfo: Partial = {}): TypeInfo => { + return { + typeNames: [typeName], + typeId: typeName, + alignment: 0, + size: 0, + canExpand: false, + hasValue: false, + arraySize: 0, + isPointer: Boolean(typeName.match(/^.+\*$/) || typeName.match(/^.+&$/)), + members: [], + enumerators: [], + ...typeProperties, + extendedInfo: { + languageId: Formatters.LanguageId.Rust, + variantParts: [], + templateParameters: [], + ...extendedInfo + }, + }; + }; + + assert.strictEqual( + CustomFormatters.get(type('T', {}, { + variantParts: [ + { + discriminatorMember: { typeId: 'i32', offset: 0 }, + variants: [ + { discriminatorValue: 1n, members: [{ typeId: 'A', offset: 4, name: '__0' }] }, + { discriminatorValue: 2n, members: [{ typeId: 'B', offset: 4, name: '__0' }] } + ] + } + ] + }))?.format, + Formatters.formatRustSumType, + ); + assert.strictEqual( + CustomFormatters.get(type('(X, Y)', { + members: [ + { typeId: 'X', offset: 0, name: '__0' }, + { typeId: 'Y', offset: 4, name: '__1' }, + ], + }))?.format, + Formatters.formatRustTuple, + ); + assert.strictEqual(CustomFormatters.get(type('&[T]'))?.format, Formatters.formatRustArraySlice); + assert.strictEqual(CustomFormatters.get(type('alloc::vec::Vec'))?.format, Formatters.formatRustVector); + assert.strictEqual(CustomFormatters.get(type('&str'))?.format, Formatters.formatRustStringSlice); + assert.strictEqual(CustomFormatters.get(type('alloc::string::String'))?.format, Formatters.formatRustString); + assert.strictEqual(CustomFormatters.get(type('alloc::rc::Rc'))?.format, Formatters.formatRustRcPointer); + }); }); diff --git a/test/SymbolsBackend.test.ts b/test/SymbolsBackend.test.ts index 293ecc2..3070506 100644 --- a/test/SymbolsBackend.test.ts +++ b/test/SymbolsBackend.test.ts @@ -6,17 +6,10 @@ import assert from 'assert'; import { describe, it } from 'node:test'; import { createModuleRunner } from '../test-utils/emscripten-module-runner'; +import SymbolsBackendPlugin from '../test-utils/SymbolsBackendPlugin'; +import TestWasmInterface from '../test-utils/TestWasmInterface'; import type { SymbolsBackendTestsModule } from '../wasm/symbols-backend/tests/SymbolsBackendTests'; -type ResponseWithError = { error?: { code: string; message: string; }; }; - -function okReponse(response: T) { - if (response.error) { - assert.fail(`Expect succesfull response, got ${response.error.code}: ${response.error.message}`); - } - return response; -} - describe('SymbolsBackend', () => { it('runs WebAssembly test suite', async () => { @@ -47,4 +40,91 @@ describe('SymbolsBackend', () => { } })); }); + + it('parses rust variant part types', async () => { + const plugin = SymbolsBackendPlugin.create(`${__dirname}/../wasm/e2e.build/app_Rust_application_0.wasm`); + try { + okReponse(plugin.AddRawModule('1', 'app_Rust_application_0.wasm')); + + const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 31, -1)); + + const debuggerProxy = new TestWasmInterface(); + debuggerProxy.memory = new SharedArrayBuffer(32); + debuggerProxy.locals.set(2, { type: 'i32', value: 4 }); + + const { root: animalType, typeInfos } = okReponse( + plugin.EvaluateExpression( + { rawModuleId: '1', codeOffset, inlineFrameIndex: 0 }, + 'a', + debuggerProxy, + ) + ); + + assert.equal(animalType.typeNames[0], 'app::Animal'); + assert.equal(animalType.size, 24); + + assert(animalType.extendedInfo); + assert.equal(animalType.extendedInfo.variantParts.length, 1); + assert(animalType.extendedInfo.variantParts[0].discriminatorMember); + assert.equal(animalType.extendedInfo.variantParts[0].variants.length, 2); + + const dogType = animalType.extendedInfo.variantParts[0].variants[0].members[0]; + assert(dogType); + assert.equal(dogType.name, 'Dog'); + assert.equal(typeInfos.find(i => i.typeId === dogType.typeId)?.typeNames[0], 'app::Animal::Dog'); + + const catType = animalType.extendedInfo.variantParts[0].variants[1].members[0]; + assert(catType); + assert.equal(catType.name, 'Cat'); + assert.equal(typeInfos.find(i => i.typeId === catType.typeId)?.typeNames[0], 'app::Animal::Cat'); + } finally { + plugin.delete(); + } + }); + + it('parses rust template parameter types', async () => { + const plugin = SymbolsBackendPlugin.create(`${__dirname}/../wasm/e2e.build/app_Rust_application_0.wasm`); + try { + okReponse(plugin.AddRawModule('1', 'app_Rust_application_0.wasm')); + + const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 35, -1)); + + const debuggerProxy = new TestWasmInterface(); + debuggerProxy.memory = new SharedArrayBuffer(32); + debuggerProxy.locals.set(2, { type: 'i32', value: 4 }); + + const { root: vectorType, typeInfos } = okReponse( + plugin.EvaluateExpression( + { rawModuleId: '1', codeOffset, inlineFrameIndex: 0 }, + 'vector', + debuggerProxy, + ) + ); + + assert.equal(vectorType.typeNames[0], 'alloc::vec::Vec'); + assert.equal(vectorType.size, 12); + + assert(vectorType.extendedInfo); + assert.equal(vectorType.extendedInfo.templateParameters.length, 2); + + const intType = vectorType.extendedInfo.templateParameters[0]; + assert(intType); + assert.equal(typeInfos.find(i => i.typeId === intType.typeId)?.typeNames[0], 'int'); + + const globalAllocType = vectorType.extendedInfo.templateParameters[1]; + assert(globalAllocType); + assert.equal(typeInfos.find(i => i.typeId === globalAllocType.typeId)?.typeNames[0], 'alloc::alloc::Global'); + } finally { + plugin.delete(); + } + }); }); + +type ResponseWithError = { error?: { code: string; message: string; }; }; + +function okReponse(response: T) { + if (response.error) { + assert.fail(`Expect succesfull response, got ${response.error.code}: ${response.error.message}`); + } + return response; +} \ No newline at end of file diff --git a/test/e2e/e2e-specs.test.ts b/test/e2e/e2e-specs.test.ts index 1987340..50b74f7 100644 --- a/test/e2e/e2e-specs.test.ts +++ b/test/e2e/e2e-specs.test.ts @@ -197,11 +197,12 @@ function testCaseOutputName(sourceFilePath: string, name: string, i: number) { async function getVariable(debuggerSession: DebuggerSession, scopedPath: string) { const [scope, name, ...propertiesPath] = scopedPath.split('.'); - assert(scope); - assert(name); + assert(scope && name, `Expected scope and variable name to be specified.`); const variableList = await debuggerSession.listVariablesInScope(); - assert(variableList.some(v => v.name === name && v.scope === scope.toUpperCase()), `Variable ${scope}.${name} does not exist.`); + assert( + variableList.some(v => v.name === name && v.scope === scope.toUpperCase()), + `Variable ${scope}.${name} does not exist, variables in scope: ${variableList.map(v => `${v.scope}.${v.name}`).join(', ')}`); let value = await debuggerSession.evaluate(name); let parent = `${scope}.${name}`; @@ -218,7 +219,7 @@ async function getVariable(debuggerSession: DebuggerSession, scopedPath: string) const property = /^\$\d+$/.test(name) ? propertiesList[parseInt(name.slice(1))] : propertiesList.find(p => p.name === name); - assert(property, `Variable ${parent}.${name} does not exist.`); + assert(property, `Property ${name} on ${parent} does not exist, available properties: ${propertiesList.map(p => p.name).join(', ')}`); value = property.value; parent = `${parent}.${name}`; diff --git a/wasm/builder/Dockerfile b/wasm/builder/Dockerfile index 0e91eec..011f5c0 100644 --- a/wasm/builder/Dockerfile +++ b/wasm/builder/Dockerfile @@ -16,9 +16,17 @@ RUN ./emsdk install "$EMSDK_VERSION" && ./emsdk activate "$EMSDK_VERSION" ENV HOST_C_COMPILER=/emsdk/upstream/bin/clang ENV HOST_CXX_COMPILER=/emsdk/upstream/bin/clang++ -WORKDIR /wasm +WORKDIR / + +# Install rust +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y +RUN /root/.cargo/bin/rustup target add wasm32-wasip1 wasm32-unknown-unknown +ENV PATH="/root/.cargo/bin:${PATH}" -# Install node modules -RUN npm i js-yaml@4.1.0 +# Install global node modules +RUN npm i -g js-yaml@4.1.0 +ENV NODE_PATH="/usr/local/lib/node_modules" + +WORKDIR /wasm ENTRYPOINT [ "/bin/sh", "build.sh" ] \ No newline at end of file diff --git a/wasm/e2e/resources/app.rs b/wasm/e2e/resources/app.rs new file mode 100644 index 0000000..dbe49dd --- /dev/null +++ b/wasm/e2e/resources/app.rs @@ -0,0 +1,65 @@ +enum Animal { + Dog(String, f64), + Cat { name: String, weight: f64 }, +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Point2D { + x: i32, + y: i32, +} + +fn print_animal_info(animal: Animal) { + match animal { + Animal::Dog(name, weight) => { + println!("Dog's name: {}, weight: {} kg", name, weight); + } + Animal::Cat { name, weight } => { + println!("Cat's name: {}, weight: {} kg", name, weight); + } + } +} + +fn main() { + let a: Animal = Animal::Dog("Biscuit".to_string(), 8.51); + let b: Animal = Animal::Cat { + name: "Whiskers".to_string(), + weight: 3.15, + }; + + print_animal_info(a); + print_animal_info(b); + + let vector = Vec::from([1, 2, 3, 4, 5]); + let vector_slice = &vector[1..3]; + let string_slice = &"Biscuit".to_string()[2..5]; + + println!("vector: {:?}", vector); + println!("vector_slice: {:?}", vector_slice); + println!("string_slice: {:?}", string_slice); + + let strong_ref = std::rc::Rc::new(Point2D { x: 20, y: 40 }); + let weak_ref_1 = std::rc::Rc::downgrade(&strong_ref); + let weak_ref_2 = std::rc::Rc::downgrade(&strong_ref); + + println!("rc pointers:"); + println!(" strong_ref: {:?}", strong_ref); + println!(" weak_ref_1: {:?}", weak_ref_1.upgrade()); + println!(" weak_ref_2: {:?}", weak_ref_2.upgrade()); + println!(" strong_count: {}", weak_ref_2.strong_count()); + println!(" weak_count: {}", weak_ref_2.weak_count()); + + drop(weak_ref_1); + println!("after drop weak_ref_1:"); + println!(" strong_ref: {:?}", strong_ref); + println!(" weak_ref_2: {:?}", weak_ref_2.upgrade()); + println!(" strong_count: {}", weak_ref_2.strong_count()); + println!(" weak_count: {}", weak_ref_2.weak_count()); + + drop(strong_ref); + println!("after drop weak_ref_1:"); + println!(" weak_ref_2: {:?}", weak_ref_2.upgrade()); + println!(" strong_count: {}", weak_ref_2.strong_count()); + println!(" weak_count: {}", weak_ref_2.weak_count()); +} diff --git a/wasm/e2e/rust_app.yaml b/wasm/e2e/rust_app.yaml new file mode 100644 index 0000000..ee4250e --- /dev/null +++ b/wasm/e2e/rust_app.yaml @@ -0,0 +1,131 @@ +name: Rust application +source_file: resources/app.rs +flags: [ + [--target, wasm32-wasip1, -g]] +only: true +script: + - reason: setup + actions: + - file: app.rs + action: set_breakpoint + breakpoint: 14 + - file: app.rs + action: set_breakpoint + breakpoint: 16 + - file: app.rs + action: set_breakpoint + breakpoint: 19 + - file: app.rs + action: set_breakpoint + breakpoint: 38 + - file: app.rs + action: set_breakpoint + breakpoint: 46 + - file: app.rs + action: set_breakpoint + breakpoint: 54 + - file: app.rs + action: set_breakpoint + breakpoint: 61 + - reason: breakpoint + file: app.rs + line: 14 + variables: + - name: Parameter.animal + type: app::Animal + - name: Parameter.animal.Dog + type: app::Animal::Dog + - name: Parameter.animal.Dog.0.string + value: '"Biscuit"' + - name: Parameter.animal.Dog.1 + value: 8.51 + - reason: breakpoint + file: app.rs + line: 16 + variables: + - name: Local.name.string + value: '"Biscuit"' + - name: Local.weight + value: 8.51 + - reason: breakpoint + file: app.rs + line: 14 + variables: + - name: Parameter.animal + type: app::Animal + - name: Parameter.animal.Cat + type: app::Animal::Cat + - name: Parameter.animal.Cat.name.string + value: '"Whiskers"' + - name: Parameter.animal.Cat.weight + value: 3.15 + - reason: breakpoint + file: app.rs + line: 19 + variables: + - name: Local.name.string + value: '"Whiskers"' + - name: Local.weight + value: 3.15 + - reason: breakpoint + file: app.rs + line: 38 + variables: + - name: Local.vector + type: 'alloc::vec::Vec' + - name: Local.vector.0 + value: 1 + - name: Local.vector.1 + value: 2 + - name: Local.vector.2 + value: 3 + - name: Local.vector.3 + value: 4 + - name: Local.vector.4 + value: 5 + - name: Local.vector_slice + type: '&[i32]' + - name: Local.vector_slice.0 + value: 2 + - name: Local.vector_slice.1 + value: 3 + - name: Local.string_slice.size + value: 3 + - name: Local.string_slice + type: '&str' + - name: Local.string_slice.string + value: '"scu"' + - reason: breakpoint + file: app.rs + line: 46 + variables: + - name: Local.strong_ref.$0 + type: app::Point2D + - name: Local.strong_ref.$0.x + value: 20 + - name: Local.strong_ref.$0.y + value: 40 + - name: Local.weak_ref_2.$0 + type: app::Point2D + - name: Local.weak_ref_2.$0.x + value: 20 + - name: Local.weak_ref_2.$0.y + value: 40 + - name: Local.weak_ref_2.strong_count + value: 1 + - name: Local.weak_ref_2.weak_count + value: 2 + - reason: breakpoint + file: app.rs + line: 54 + variables: + - name: Local.weak_ref_2.strong_count + value: 1 + - name: Local.weak_ref_2.weak_count + value: 1 + - reason: breakpoint + file: app.rs + line: 61 + variables: + - name: Local.weak_ref_2.$0 + value: null \ No newline at end of file diff --git a/wasm/generate-e2e-resources-build b/wasm/generate-e2e-resources-build index 3462a3f..1a1eb97 100755 --- a/wasm/generate-e2e-resources-build +++ b/wasm/generate-e2e-resources-build @@ -6,6 +6,17 @@ const yaml = require('js-yaml'); const out = process.stdout.write.bind(process.stdout); const err = process.stderr.write.bind(process.stderr); +if (process.argv[2] === '--wasi-p1-runner' && process.argv[3]) { + out(`module.exports = () => {\n`); + out(` const fs = require('fs');\n`); + out(` const { WASI } = require('node:wasi');\n`); + out(` const wasi = new WASI({ preopens: {}, returnOnExit: false, version: 'preview1' });\n`); + out(` const binary = fs.readFileSync(__dirname + '/${process.argv[3]}');\n`); + out(` WebAssembly.instantiate(binary, wasi.getImportObject()).then(({ instance }) => wasi.start(instance));\n`); + out(`}`); + process.exit(0); +} + const inputFiles = process.argv.slice(2); out(`rule build_dwp\n`); @@ -14,6 +25,10 @@ out(` description = Generating dwp file, creating $out\n\n`); out(`rule build_wasm\n`); out(` command = $compiler $flags $in -o $out\n`); out(` description = Compiling file '$in' with flags: "$flags"\n\n`); +out(`rule build_wasip1_runner\n`); +out(` command = ${__filename} --wasi-p1-runner $in > $out\n`); +out(` description = Generating wasip1 runner, creating $out"\n\n`); + for (const specFilePath of inputFiles) { const { name, source_file: sourceFile, flags, use_dwo: useDwo } = yaml.load(fs.readFileSync(specFilePath, 'utf8')); @@ -25,10 +40,20 @@ for (const specFilePath of inputFiles) { const sourceFilePath = resolvePath(specFilePath, sourceFile); assert(specFilePath, fs.existsSync(sourceFilePath), `File does not exist: ${sourceFile}`); + const sourceFileIsRust = '.rs' === path.extname(sourceFilePath).toLowerCase(); const sourceFileIsCpp = ['.cc', '.cpp'].some(extname => extname === path.extname(sourceFilePath).toLowerCase()); for (let i = 0; i < flags.length; i++) { const outputFile = testCaseOutputName(sourceFilePath, name, i); + + if (sourceFileIsRust) { + out(`build ${outputFile}.wasm: build_wasm ${encodeFilePath(sourceFilePath)}\n`); + out(` compiler = rustc\n`); + out(` flags = ${flags[i].join(' ')}\n\n`); + out(`build ${outputFile}.js: build_wasip1_runner ${outputFile}.wasm\n\n`); + continue; + } + const compiler = `${process.env['EMSDK']}/upstream/emscripten/${sourceFileIsCpp ? 'em++' : 'emcc'}`; const compileFlags = flags[i].filter(f => !f.startsWith('-s')).join(' '); const linkFlags = ['-sMODULARIZE=1', '-sEXIT_RUNTIME=1', '-sENVIRONMENT=node', ...flags[i]].join(' '); diff --git a/wasm/symbols-backend/lib/ApiContext.cc b/wasm/symbols-backend/lib/ApiContext.cc index b024fa5..16d094c 100644 --- a/wasm/symbols-backend/lib/ApiContext.cc +++ b/wasm/symbols-backend/lib/ApiContext.cc @@ -6,6 +6,7 @@ #include "Expressions.h" #include "Variables.h" #include "WasmModule.h" +#include "WasmVendorPlugins.h" #include "api.h" #include "lldb/Symbol/CompilerType.h" @@ -15,6 +16,7 @@ #include "llvm/ADT/APFloat.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" @@ -26,7 +28,6 @@ #include "llvm/Support/FileSystem.h" #include "llvm/Support/raw_ostream.h" -#include #include #include #include @@ -43,6 +44,16 @@ namespace api { namespace { +template +std::vector MapToVector(const llvm::SmallVector input, + std::function transform) { + std::vector output; + for (auto element : input) { + output.push_back(transform(element)); + } + return std::move(output); +} + Error MakeError(Error::Code ec, llvm::Twine message) { Error e; e.SetCode(ec); @@ -371,6 +382,7 @@ api::TypeInfo ApiContext::GetApiTypeInfo( return true; }); + auto type_system = type.GetTypeSystem().dyn_cast_or_null(); auto size = type.GetByteSize(nullptr); uint64_t array_size = 0; size_t alignment = type.GetTypeBitAlign(nullptr).value_or(0) / 8; @@ -399,7 +411,8 @@ api::TypeInfo ApiContext::GetApiTypeInfo( .SetIsPointer(type.IsPointerOrReferenceType(nullptr)) .SetAlignment(alignment) .SetMembers(std::move(members)) - .SetEnumerators(std::move(enumerators)); + .SetEnumerators(std::move(enumerators)) + .SetExtendedInfo(GetApiExtendedTypeInfo(type)); } llvm::Expected> ApiContext::GetApiTypeInfos( @@ -424,9 +437,25 @@ llvm::Expected> ApiContext::GetApiTypeInfos( auto member_info = SubObjectInfo::GetMembers(type); for (const auto& member : member_info) { if (!visited_types.contains(member.Type().GetOpaqueQualType())) { - queue.push_back({member.Type(), depth + 1}); + queue.push_back({ member.Type(), depth + 1 }); } } + + auto extended_info = TypeSystemClangExtended::GetExtendedTypeInfo(type); + if (extended_info) { + for (auto variant_part : extended_info->variant_parts) { + queue.push_back({variant_part.discr_member.type, depth + 1}); + for (auto variant : variant_part.variants) { + for (auto member : variant.members) { + queue.push_back({member.type, depth + 1}); + } + } + } + for (auto parameter : extended_info->template_parameters) { + queue.push_back({parameter.type, depth + 1}); + } + } + type_infos.push_back(GetApiTypeInfo(type, member_info)); } return type_infos; @@ -448,6 +477,55 @@ llvm::Optional ApiContext::GetTypeFromId( return it->getValue(); } +llvm::Optional +ApiContext::GetApiExtendedTypeInfo(lldb_private::CompilerType type) { + auto extended_info = TypeSystemClangExtended::GetExtendedTypeInfo(type); + if (!extended_info) { + return llvm::None; + } + + auto variant_parts = + MapToVector( + extended_info->variant_parts, [&](auto part) { + auto discriminator_member = + api::FieldInfo() + .SetName(part.discr_member.name) + .SetOffset(part.discr_member.location) + .SetTypeId(GetTypeId(part.discr_member.type)); + + auto variants = MapToVector( + part.variants, [&](auto variant) { + return api::VariantInfo() + .SetDiscriminatorValue(variant.discr_value) + .SetMembers( + MapToVector( + variant.members, [&](auto member) { + return api::FieldInfo() + .SetName(member.name) + .SetOffset(member.location) + .SetTypeId(GetTypeId(member.type)); + })); + }); + + return api::VariantPartInfo() + .SetDiscriminatorMember(std::move(discriminator_member)) + .SetVariants(std::move(variants)); + }); + + auto template_parameters = + MapToVector( + extended_info->template_parameters, [&](auto parameter) { + return api::TemplateParameterInfo() + .SetName(parameter.name) + .SetTypeId(GetTypeId(parameter.type)); + }); + + return std::move(api::ExtendedTypeInfo() + .SetLanguageId(extended_info->language) + .SetVariantParts(std::move(variant_parts)) + .SetTemplateParameters(std::move(template_parameters))); +} + struct EvalVisitor { ApiContext& context; lldb_private::CompilerType type; @@ -611,17 +689,20 @@ api::EvaluateExpressionResponse ApiContext::EvaluateExpression( } // namespace symbols_backend #ifndef NDEBUG + +#include "lldb/Utility/LLDBLog.h" + namespace { struct Logging { Logging() { - lldb_private::Log::Initialize(); + lldb_private::InitializeLldbChannel(); lldb_private::Log::ListAllLogChannels(llvm::errs()); - auto stream = std::make_shared(2, false, true); - lldb_private::Log::EnableLogChannel(stream, 0, "lldb", {"default"}, + auto handler = std::make_shared(2, false); + lldb_private::Log::EnableLogChannel(handler, 0, "lldb", {"default"}, llvm::errs()); } }; static Logging l; -} // namespace +} // namespace #endif diff --git a/wasm/symbols-backend/lib/ApiContext.h b/wasm/symbols-backend/lib/ApiContext.h index 2643e63..d0af6ad 100644 --- a/wasm/symbols-backend/lib/ApiContext.h +++ b/wasm/symbols-backend/lib/ApiContext.h @@ -14,7 +14,6 @@ #include "llvm/ADT/StringRef.h" #include "llvm/BinaryFormat/Wasm.h" #include "llvm/Support/Error.h" -#include "llvm/Support/JSON.h" #include #include @@ -109,6 +108,8 @@ class ApiContext : public DWARFSymbolsApi { std::string GetTypeId(lldb_private::CompilerType type); llvm::Optional GetTypeFromId( llvm::StringRef type_id); + llvm::Optional + GetApiExtendedTypeInfo(lldb_private::CompilerType type); friend struct EvalVisitor; }; diff --git a/wasm/symbols-backend/lib/Variables.cc b/wasm/symbols-backend/lib/Variables.cc index 874e1aa..e682381 100644 --- a/wasm/symbols-backend/lib/Variables.cc +++ b/wasm/symbols-backend/lib/Variables.cc @@ -14,7 +14,6 @@ #include #include #include -#include #include namespace symbols_backend { diff --git a/wasm/symbols-backend/lib/WasmModule.cc b/wasm/symbols-backend/lib/WasmModule.cc index c97703a..570dca2 100644 --- a/wasm/symbols-backend/lib/WasmModule.cc +++ b/wasm/symbols-backend/lib/WasmModule.cc @@ -4,10 +4,8 @@ #include "WasmModule.h" #include "Expressions.h" -#include "WasmVendorPlugins.h" #include "Plugins/SymbolFile/DWARF/DWARFCompileUnit.h" -#include "Plugins/SymbolFile/DWARF/LogChannelDWARF.h" #include "lldb/Core/Address.h" #include "lldb/Core/AddressRange.h" #include "lldb/Core/Debugger.h" @@ -25,13 +23,11 @@ #include "lldb/Symbol/SymbolContextScope.h" #include "lldb/Symbol/Type.h" #include "lldb/Symbol/TypeList.h" -#include "lldb/Symbol/TypeSystem.h" #include "lldb/Symbol/Variable.h" #include "lldb/Symbol/VariableList.h" #include "lldb/Utility/ArchSpec.h" #include "lldb/Utility/ConstString.h" #include "lldb/Utility/FileSpec.h" -#include "lldb/Utility/Log.h" #include "lldb/lldb-enumerations.h" #include "lldb/lldb-forward.h" #include "llvm/ADT/Optional.h" diff --git a/wasm/symbols-backend/lib/WasmModule.h b/wasm/symbols-backend/lib/WasmModule.h index fc26809..44c378a 100644 --- a/wasm/symbols-backend/lib/WasmModule.h +++ b/wasm/symbols-backend/lib/WasmModule.h @@ -4,6 +4,7 @@ #ifndef EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ #define EXTENSIONS_CXX_DEBUGGING_WASM_MODULE_H_ + #include "lldb/lldb-enumerations.h" #include "lldb/lldb-forward.h" #include "lldb/lldb-types.h" @@ -111,6 +112,7 @@ class WasmModule { lldb::VariableSP FindVariableAtOffset(lldb::addr_t offset, int inline_frame_index, llvm::StringRef name) const; + llvm::Optional FindType( llvm::StringRef name) const; diff --git a/wasm/symbols-backend/lib/WasmVendorPlugins.cc b/wasm/symbols-backend/lib/WasmVendorPlugins.cc index b0b7b76..9c4d9b9 100644 --- a/wasm/symbols-backend/lib/WasmVendorPlugins.cc +++ b/wasm/symbols-backend/lib/WasmVendorPlugins.cc @@ -4,11 +4,32 @@ #include "WasmVendorPlugins.h" +#include "Plugins/ExpressionParser/Clang/ClangPersistentVariables.h" +#include "Plugins/SymbolFile/DWARF/DWARFASTParserClang.h" +#include "Plugins/SymbolFile/DWARF/DWARFAttribute.h" +#include "Plugins/SymbolFile/DWARF/DWARFDIE.h" +#include "Plugins/SymbolFile/DWARF/DWARFUnit.h" #include "Plugins/SymbolFile/DWARF/LogChannelDWARF.h" #include "lldb/Core/PluginManager.h" +#include "lldb/Core/dwarf.h" #include "lldb/Host/linux/HostInfoLinux.h" +#include "lldb/Symbol/Type.h" #include "lldb/Target/Platform.h" #include "lldb/Utility/RegisterValue.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" +#include "lldb/lldb-types.h" +#include "clang/AST/DeclBase.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" +#include "llvm/BinaryFormat/Dwarf.h" +#include "llvm/DebugInfo/DWARF/DWARFFormValue.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/Format.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include namespace symbols_backend { @@ -147,7 +168,344 @@ lldb_private::SymbolFile* SymbolFileWasmDWARF::CreateInstance( return new SymbolFileWasmDWARF(std::move(objfile_sp), /*dwo_section_list*/ nullptr); } -} // namespace symbols_backend + +using namespace llvm::dwarf; + +namespace { + +void ForEachDWARFDIEChild(DWARFDIE die, dw_tag_t tag, + std::function callback) { + die = die.GetFirstChild(); + while (die) { + if (die.Tag() == tag) { + callback(die); + } + die = die.GetSibling(); + } +} + +llvm::Optional GetRecordByteSize(const DWARFDIE &die) { + switch (die.Tag()) { + case DW_TAG_variant_part: + case DW_TAG_structure_type: + case DW_TAG_union_type: + case DW_TAG_class_type: + break; + default: + return llvm::None; + } + auto byte_size = die.GetAttributeValueAsOptionalUnsigned(DW_AT_byte_size); + if (!byte_size) { + byte_size = die.GetAttributeValueAsOptionalUnsigned(DW_AT_bit_size) + .transform([](auto value) { return (value + 7) / 8; }); + } + + if (!byte_size || byte_size.value() > UINT32_MAX) { + return llvm::None; + } + + return (uint32_t)byte_size.value(); +} + +bool ExtractTypeFromDWARFDIE(const DWARFDIE &die, + lldb_private::Type **type_value) { + auto type_die = die.GetAttributeValueAsReferenceDIE(DW_AT_type); + if (!type_die) { + llvm::errs() << "ExtractTypeFromDWARFDIE: DW_AT_type reference is " + "missing or not " + "valid for " + << llvm::format_hex(die.GetOffset(), 10) + << ", ignoring entry.\n"; + return false; + } + + auto type = type_die.ResolveType(); + if (!type) { + llvm::errs() + << "ExtractTypeFromDWARFDIE: DW_AT_type reference could not be " + "resolved to a type for " + << llvm::format_hex(die.GetOffset(), 10) << ", ignoring entry.\n"; + return false; + } + + *type_value = type; + return true; +} + +bool ExtractMemberInfo(const DWARFDIE &die, types::MemberInfo &info) { + + auto location = + die.GetAttributeValueAsOptionalUnsigned(DW_AT_data_member_location); + if (!location) { + llvm::errs() + << "ExtractMemberInfo: DW_AT_data_member_location is missing for " + << llvm::format_hex(die.GetOffset(), 10) << ", ignoring entry.\n"; + return false; + } + if (location.value() > UINT32_MAX) { + llvm::errs() + << "ExtractMemberInfo: DW_AT_data_member_location > UINT32_MAX for " + << llvm::format_hex(die.GetOffset(), 10) << ", ignoring entry.\n"; + return false; + } + + lldb_private::Type *type; + if (!ExtractTypeFromDWARFDIE(die, &type)) { + return false; + } + info.name = die.GetAttributeValueAsString(DW_AT_name, ""); + info.location = (uint32_t)location.value(); + info.type = type->GetForwardCompilerType(); + return true; +} + +bool ExtractVariantInfo(const DWARFDIE &die, types::VariantInfo &info) { + + info.discr_value = die.GetAttributeValueAsOptionalUnsigned(DW_AT_discr_value); + + ForEachDWARFDIEChild(die, DW_TAG_member, [&info](auto member_die) { + types::MemberInfo member; + if (ExtractMemberInfo(member_die, member)) { + info.members.push_back(member); + } + }); + + if (info.members.empty()) { + llvm::errs() << "ExtractVariantInfo: Missing or only non valid " + "DW_TAG_member children for " + << llvm::format_hex(die.GetOffset(), 10) + << ", ignoring entry.\n"; + return false; + } + + return true; +} + +bool ExtractVariantPartInfo(const DWARFDIE &die, types::VariantPartInfo &info) { + + auto discr_member_die = die.GetAttributeValueAsReferenceDIE(DW_AT_discr); + + if (!discr_member_die) { + llvm::errs() + << "ExtractVariantPartInfo: DW_AT_discr is missing or not valid for " + << llvm::format_hex(die.GetOffset(), 10) << ", ignoring entry.\n"; + return false; + } + + if (!ExtractMemberInfo(discr_member_die, info.discr_member)) { + return false; + } + + ForEachDWARFDIEChild(die, DW_TAG_variant, [&info](auto variant_die) { + types::VariantInfo variant; + if (ExtractVariantInfo(variant_die, variant)) { + info.variants.push_back(variant); + } + }); + + if (info.variants.empty()) { + llvm::errs() << "ExtractVariantPartInfo: Missing or only non valid " + "DW_TAG_variant children for " + << llvm::format_hex(die.GetOffset(), 10) + << ", ignoring entry.\n"; + return false; + } + + return true; +} + +bool ExtractTemplateParameterInfo(const DWARFDIE &die, + types::TemplateParameterInfo &info) { + + lldb_private::Type *type; + if (!ExtractTypeFromDWARFDIE(die, &type)) { + return false; + } + + auto name = die.GetAttributeValueAsString(DW_AT_name, nullptr); + + info.type = type->GetForwardCompilerType(); + info.name = name ? llvm::Optional(name) : llvm::None; + return true; +} + +void LinkVariantPartMemberTypesToDeclContext( + const DWARFDIE &die, + std::function link_member_type) { + + auto link_member = [&link_member_type](auto die) { + DWARFDIE type = die.GetAttributeValueAsReferenceDIE(DW_AT_type); + if (!type) { + llvm::errs() << "LinkVariantPartMemberTypesToDeclContext: missing " + "DW_AT_type for " + << llvm::format_hex(die.GetOffset(), 10) + << ", ignoring entry.\n"; + return; + } + link_member_type(type); + }; + + auto link_variant = [&link_member](auto die) { + ForEachDWARFDIEChild(die, DW_TAG_member, link_member); + }; + + auto link_variant_part = [&link_variant](auto die) { + ForEachDWARFDIEChild(die, DW_TAG_variant, link_variant); + }; + + ForEachDWARFDIEChild(die, DW_TAG_variant_part, link_variant_part); +} + +bool IsLanguageSupportedByExtendedTypeInfo(lldb::LanguageType language) { + switch (language) { + case lldb::eLanguageTypeRust: + return true; + default: + return false; + } +} + +} // namespace + +bool DWARFASTParserClangExtended::CompleteTypeFromDWARF( + const DWARFDIE &die, lldb_private::Type *type, + lldb_private::CompilerType &compiler_type) { + + auto dieCU = die.GetCU(); + auto language = dieCU ? (lldb::LanguageType)dieCU->GetDWARFLanguageType() + : compiler_type.GetMinimumLanguage(); + + if (IsLanguageSupportedByExtendedTypeInfo(language)) { + auto decl_context = + m_ast.GetAsCXXRecordDecl(compiler_type.GetOpaqueQualType()); + if (decl_context) { + auto link_member_type = [&](auto type_die) { + LinkDeclContextToDIE(decl_context, type_die); + }; + LinkVariantPartMemberTypesToDeclContext(die, link_member_type); + } + } + + if (!DWARFASTParserClang::CompleteTypeFromDWARF(die, type, compiler_type)) { + return false; + } + + if (IsLanguageSupportedByExtendedTypeInfo(language)) { + auto type_info = + TypeSystemClangExtended::GetExtendedTypeInfo(compiler_type, true); + + type_info->language = language; + + ForEachDWARFDIEChild( + die, DW_TAG_variant_part, [&type_info](auto variant_part_die) { + types::VariantPartInfo variant_part; + if (ExtractVariantPartInfo(variant_part_die, variant_part)) { + type_info->variant_parts.push_back(variant_part); + } + }); + + ForEachDWARFDIEChild( + die, DW_TAG_template_type_parameter, + [&type_info](auto template_parameter_die) { + types::TemplateParameterInfo template_parameter; + if (ExtractTemplateParameterInfo(template_parameter_die, + template_parameter)) { + type_info->template_parameters.push_back(template_parameter); + } + }); + + type_info->byte_size = GetRecordByteSize(die); + } + + return true; +} + +/// LLVM RTTI support. +char TypeSystemClangExtended::ID; +char ClangExternalASTSourceCallbacks::ID; + +TypeSystemClangExtended::TypeSystemClangExtended(llvm::StringRef name, + llvm::Triple triple) + : lldb_private::TypeSystemClang(name, triple) { + llvm::IntrusiveRefCntPtr ast_source_up( + new ClangExternalASTSourceCallbacks(*this)); + SetExternalSource(ast_source_up); +} + +DWARFASTParser *TypeSystemClangExtended::GetDWARFParser() { + if (!m_dwarf_ast_parser_up) + m_dwarf_ast_parser_up = + std::make_unique(*this); + return m_dwarf_ast_parser_up.get(); +} + +void TypeSystemClangExtended::Initialize() { + lldb_private::PluginManager::RegisterPlugin( + GetPluginNameStatic(), + "clang base AST context plug-in (with extended rust support)", + CreateInstance, GetSupportedLanguagesForTypes(), + GetSupportedLanguagesForExpressions()); +} + +void TypeSystemClangExtended::Terminate() { + lldb_private::PluginManager::UnregisterPlugin(CreateInstance); +} + +llvm::Optional TypeSystemClangExtended::GetBitSize( + lldb::opaque_compiler_type_t type, + lldb_private::ExecutionContextScope *exe_scope) { + auto extended_info = GetExtendedTypeInfo(type); + if (extended_info && extended_info->byte_size) { + return extended_info->byte_size.value() * 8ULL; + } + + return TypeSystemClang::GetBitSize(type, exe_scope); +} + +lldb::TypeSystemSP +TypeSystemClangExtended::CreateInstance(lldb::LanguageType language, + lldb_private::Module *module, + lldb_private::Target *target) { + if (!module) { + return TypeSystemClang::CreateInstance(language, nullptr, target); + } + + return std::make_shared( + "ASTContext for '" + module->GetFileSpec().GetPath() + "'", + module->GetArchitecture().GetTriple()); +} + +types::ExtendedTypeInfo * +TypeSystemClangExtended::GetExtendedTypeInfo(lldb_private::CompilerType type, + bool create_if_needed) { + auto type_system = + type.GetTypeSystem().dyn_cast_or_null(); + if (!type_system) { + return nullptr; + } + + return type_system->GetExtendedTypeInfo(type.GetOpaqueQualType(), + create_if_needed); +} + +types::ExtendedTypeInfo * +TypeSystemClangExtended::GetExtendedTypeInfo(lldb::opaque_compiler_type_t type, + bool create_if_needed) { + + auto entry = m_type_info.find(type); + if (entry != m_type_info.end()) { + return &entry->second; + } + + if (!create_if_needed) { + return nullptr; + } + + return &m_type_info.insert(std::make_pair(type, types::ExtendedTypeInfo())) + .first->second; +} + +} // namespace symbols_backend LLDB_PLUGIN_DEFINE_ADV(symbols_backend::SymbolFileWasmDWARF, SymbolFileWasmDWARF) diff --git a/wasm/symbols-backend/lib/WasmVendorPlugins.h b/wasm/symbols-backend/lib/WasmVendorPlugins.h index 00348af..0cd0740 100644 --- a/wasm/symbols-backend/lib/WasmVendorPlugins.h +++ b/wasm/symbols-backend/lib/WasmVendorPlugins.h @@ -4,13 +4,20 @@ #ifndef EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ #define EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ + #include "ApiContext.h" +#include "Plugins/SymbolFile/DWARF/DWARFASTParserClang.h" #include "Plugins/SymbolFile/DWARF/DWARFDeclContext.h" #include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h" #include "Plugins/TypeSystem/Clang/TypeSystemClang.h" +#include "Plugins/ExpressionParser/Clang/ClangExternalASTSourceCallbacks.h" #include "lldb/Core/Debugger.h" +#include "lldb/Symbol/TypeSystem.h" #include "lldb/Target/RegisterContext.h" #include "lldb/Target/Unwind.h" +#include "lldb/lldb-forward.h" +#include "lldb/lldb-types.h" +#include "llvm/ADT/Optional.h" #include "llvm/BinaryFormat/Dwarf.h" class SymbolVendorWASM; @@ -324,6 +331,127 @@ class SymbolFileWasmDWARF : public ::SymbolFileDWARF { WasmValueLoader* current_value_loader_ = nullptr; }; +namespace types { + +struct MemberInfo { + std::string name; + uint32_t location; + lldb_private::CompilerType type; +}; + +struct VariantInfo { + llvm::Optional discr_value; + llvm::SmallVector members; +}; + +struct VariantPartInfo { + MemberInfo discr_member; + llvm::SmallVector variants; +}; + +struct TemplateParameterInfo { + lldb_private::CompilerType type; + llvm::Optional name; +}; + +struct ExtendedTypeInfo { + lldb::LanguageType language; + llvm::SmallVector variant_parts; + llvm::SmallVector template_parameters; + llvm::Optional byte_size; +}; + +} // namespace types + +class DWARFASTParserClangExtended : public DWARFASTParserClang { +public: + DWARFASTParserClangExtended(lldb_private::TypeSystemClang &ast) + : DWARFASTParserClang(ast) {} + + bool + CompleteTypeFromDWARF(const DWARFDIE &die, lldb_private::Type *type, + lldb_private::CompilerType &compiler_type) override; +}; + +class TypeSystemClangExtended : public lldb_private::TypeSystemClang { + // LLVM RTTI support + static char ID; + +public: + // LLVM RTTI support + bool isA(const void *ClassID) const override { + return ClassID == &ID || lldb_private::TypeSystemClang::isA(ClassID); + } + static bool classof(const TypeSystem *ts) { return ts->isA(&ID); } + + explicit TypeSystemClangExtended(llvm::StringRef name, llvm::Triple triple); + + DWARFASTParser *GetDWARFParser() override; + + static void Initialize(); + static void Terminate(); + + static lldb::TypeSystemSP CreateInstance(lldb::LanguageType language, + lldb_private::Module *module, + lldb_private::Target *target); + + llvm::Optional + GetBitSize(lldb::opaque_compiler_type_t type, + lldb_private::ExecutionContextScope *exe_scope) override; + + static types::ExtendedTypeInfo * + GetExtendedTypeInfo(lldb_private::CompilerType type, + bool create_if_needed = false); + types::ExtendedTypeInfo * + GetExtendedTypeInfo(lldb::opaque_compiler_type_t type, + bool create_if_needed = false); + +private: + llvm::Optional m_language; + std::unique_ptr m_dwarf_ast_parser_up; + std::map m_type_info; +}; + +// We need to extend ClangExternalASTSourceCallbacks to make sure that the methods in the +// base TypeSystemClang class access the instance of ClangASTImporter held by our own +// DWARFASTParserClangExtended instead of the instance of DWARFASTParserClang declared as +// as private field on TypeSystemClang. +// TypeSystemClang::LayoutRecordType does not honour the fact that we override GetDWARFParser() +// and still uses it's private field m_dwarf_ast_parser_up which causes the parsed types +// to not include the correct size and field layout information. +class ClangExternalASTSourceCallbacks + : public lldb_private::ClangExternalASTSourceCallbacks { + /// LLVM RTTI support. + static char ID; + +public: + /// LLVM RTTI support. + bool isA(const void *ClassID) const override { + return ClassID == &ID || + lldb_private::ClangExternalASTSourceCallbacks::isA(ClassID); + } + static bool classof(const clang::ExternalASTSource *s) { return s->isA(&ID); } + + explicit ClangExternalASTSourceCallbacks(TypeSystemClangExtended &ast) + : lldb_private::ClangExternalASTSourceCallbacks(ast), + m_ast_parser( + static_cast(ast.GetDWARFParser())) {} + + bool layoutRecordType( + const clang::RecordDecl *Record, uint64_t &Size, uint64_t &Alignment, + llvm::DenseMap &FieldOffsets, + llvm::DenseMap + &BaseOffsets, + llvm::DenseMap + &VirtualBaseOffsets) override { + return m_ast_parser->GetClangASTImporter().LayoutRecordType( + Record, Size, Alignment, FieldOffsets, BaseOffsets, VirtualBaseOffsets); + } + +private: + DWARFASTParserClangExtended *m_ast_parser; +}; + } // namespace symbols_backend #endif // EXTENSIONS_CXX_DEBUGGING_WASMVENDORPLUGINS_H_ diff --git a/wasm/symbols-backend/lib/api.h b/wasm/symbols-backend/lib/api.h index 5265b20..957fa11 100644 --- a/wasm/symbols-backend/lib/api.h +++ b/wasm/symbols-backend/lib/api.h @@ -8,12 +8,12 @@ #ifndef EXTENSIONS_CXX_DEBUGGING_API_H_ #define EXTENSIONS_CXX_DEBUGGING_API_H_ +#include "llvm/ADT/Optional.h" +#include #include #include #include -#include "llvm/ADT/Optional.h" - namespace symbols_backend { namespace api { // Error details @@ -222,18 +222,101 @@ class Enumerator { } }; +class VariantInfo { + llvm::Optional discriminator_value_ = {}; + std::vector members_ = {}; + +public: + VariantInfo() = default; + virtual ~VariantInfo() = default; + llvm::Optional GetDiscriminatorValue() const { return discriminator_value_; } + VariantInfo &SetDiscriminatorValue(llvm::Optional value) { + discriminator_value_ = std::move(value); + return *this; + } + std::vector GetMembers() const { return members_; } + VariantInfo &SetMembers(std::vector value) { + members_ = std::move(value); + return *this; + } +}; + +class VariantPartInfo { + FieldInfo discriminator_member_; + std::vector variants_; + +public: + VariantPartInfo() = default; + virtual ~VariantPartInfo() = default; + FieldInfo GetDiscriminatorMember() const { return discriminator_member_; } + VariantPartInfo &SetDiscriminatorMember(FieldInfo value) { + discriminator_member_ = std::move(value); + return *this; + } + std::vector GetVariants() const { return variants_; } + VariantPartInfo &SetVariants(std::vector value) { + variants_ = std::move(value); + return *this; + } +}; + +class TemplateParameterInfo { + std::string type_id_ = {}; + llvm::Optional name_; + +public: + TemplateParameterInfo() = default; + virtual ~TemplateParameterInfo() = default; + std::string GetTypeId() const { return type_id_; } + TemplateParameterInfo &SetTypeId(std::string value) { + type_id_ = std::move(value); + return *this; + } + llvm::Optional GetName() const { return name_; } + TemplateParameterInfo &SetName(llvm::Optional value) { + name_ = std::move(value); + return *this; + } +}; + +class ExtendedTypeInfo { + uint16_t language_id_; + std::vector variant_parts_; + std::vector template_parameters_; + +public: + ExtendedTypeInfo() = default; + virtual ~ExtendedTypeInfo() = default; + uint16_t GetLanguageId() const { return language_id_; } + ExtendedTypeInfo &SetLanguageId(uint16_t value) { + language_id_ = std::move(value); + return *this; + } + std::vector GetVariantParts() const { return variant_parts_; } + ExtendedTypeInfo &SetVariantParts(std::vector value) { + variant_parts_ = std::move(value); + return *this; + } + std::vector GetTemplateParameters() const { return template_parameters_; } + ExtendedTypeInfo &SetTemplateParameters(std::vector value) { + template_parameters_ = std::move(value); + return *this; + } +}; + class TypeInfo { private: std::vector type_names_ = {}; std::string type_id_ = {}; - int32_t alignment_ = {}; - int32_t size_ = {}; + uint32_t alignment_ = {}; + uint32_t size_ = {}; bool can_expand_ = {}; bool has_value_ = {}; - llvm::Optional array_size_ = {}; + uint32_t array_size_ = {}; bool is_pointer_ = {}; std::vector members_ = {}; std::vector enumerators_ = {}; + llvm::Optional extended_info_ = {}; public: TypeInfo() = default; @@ -248,13 +331,13 @@ class TypeInfo { type_id_ = std::move(value); return *this; } - int32_t GetAlignment() const { return alignment_; } - TypeInfo &SetAlignment(int32_t value) { + uint32_t GetAlignment() const { return alignment_; } + TypeInfo &SetAlignment(uint32_t value) { alignment_ = std::move(value); return *this; } - int32_t GetSize() const { return size_; } - TypeInfo &SetSize(int32_t value) { + uint32_t GetSize() const { return size_; } + TypeInfo &SetSize(uint32_t value) { size_ = std::move(value); return *this; } @@ -268,8 +351,8 @@ class TypeInfo { has_value_ = std::move(value); return *this; } - llvm::Optional GetArraySize() const { return array_size_; } - TypeInfo &SetArraySize(llvm::Optional value) { + uint32_t GetArraySize() const { return array_size_; } + TypeInfo &SetArraySize(uint32_t value) { array_size_ = std::move(value); return *this; } @@ -288,6 +371,11 @@ class TypeInfo { enumerators_ = std::move(value); return *this; } + llvm::Optional GetExtendedInfo() const { return extended_info_; } + TypeInfo &SetExtendedInfo(llvm::Optional value) { + extended_info_ = std::move(value); + return *this; + } }; // Return type of the AddRawModule command diff --git a/wasm/symbols-backend/src/SymbolsBackend.cc b/wasm/symbols-backend/src/SymbolsBackend.cc index 2ffe6ac..3fb03f2 100644 --- a/wasm/symbols-backend/src/SymbolsBackend.cc +++ b/wasm/symbols-backend/src/SymbolsBackend.cc @@ -16,6 +16,7 @@ #include "Plugins/ScriptInterpreter/None/ScriptInterpreterNone.h" #include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h" #include "Plugins/SymbolVendor/wasm/SymbolVendorWasm.h" +#include "api.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/linux/HostInfoLinux.h" #include "llvm/ADT/StringRef.h" @@ -32,7 +33,7 @@ struct DefaultPluginsContext lldb_private::ScriptInterpreterNone, lldb_private::FileSystem, lldb_private::CPlusPlusLanguage, - lldb_private::TypeSystemClang, + symbols_backend::TypeSystemClangExtended, lldb_private::wasm::ObjectFileWasm, lldb_private::wasm::SymbolVendorWasm, symbols_backend::WasmProcess, @@ -149,6 +150,31 @@ EMSCRIPTEN_BINDINGS(DWARFSymbolsPlugin) { .property("typeId", &symbols_backend::api::Enumerator::GetTypeId, &symbols_backend::api::Enumerator::SetTypeId) ; + emscripten::class_("VariantPartInfo") + .constructor<>() + .property("discriminatorMember", &symbols_backend::api::VariantPartInfo::GetDiscriminatorMember, &symbols_backend::api::VariantPartInfo::SetDiscriminatorMember) + .property("variants", &symbols_backend::api::VariantPartInfo::GetVariants, &symbols_backend::api::VariantPartInfo::SetVariants) + ; + + emscripten::class_("VariantInfo") + .constructor<>() + .property("discriminatorValue", OptionalGetter(&symbols_backend::api::VariantInfo::GetDiscriminatorValue), OptionalSetter(&symbols_backend::api::VariantInfo::SetDiscriminatorValue)) + .property("members", &symbols_backend::api::VariantInfo::GetMembers, &symbols_backend::api::VariantInfo::SetMembers) + ; + + emscripten::class_("TemplateParameterInfo") + .constructor<>() + .property("typeId", &symbols_backend::api::TemplateParameterInfo::GetTypeId, &symbols_backend::api::TemplateParameterInfo::SetTypeId) + .property("name", OptionalGetter(&symbols_backend::api::TemplateParameterInfo::GetName), OptionalSetter(&symbols_backend::api::TemplateParameterInfo::SetName)) + ; + + emscripten::class_("ExtendedTypeInfo") + .constructor<>() + .property("languageId", &symbols_backend::api::ExtendedTypeInfo::GetLanguageId, &symbols_backend::api::ExtendedTypeInfo::SetLanguageId) + .property("variantParts", &symbols_backend::api::ExtendedTypeInfo::GetVariantParts, &symbols_backend::api::ExtendedTypeInfo::SetVariantParts) + .property("templateParameters", &symbols_backend::api::ExtendedTypeInfo::GetTemplateParameters, &symbols_backend::api::ExtendedTypeInfo::SetTemplateParameters) + ; + emscripten::class_("TypeInfo") .constructor<>() .property("typeNames", &symbols_backend::api::TypeInfo::GetTypeNames, &symbols_backend::api::TypeInfo::SetTypeNames) @@ -157,10 +183,11 @@ EMSCRIPTEN_BINDINGS(DWARFSymbolsPlugin) { .property("size", &symbols_backend::api::TypeInfo::GetSize, &symbols_backend::api::TypeInfo::SetSize) .property("canExpand", &symbols_backend::api::TypeInfo::GetCanExpand, &symbols_backend::api::TypeInfo::SetCanExpand) .property("hasValue", &symbols_backend::api::TypeInfo::GetHasValue, &symbols_backend::api::TypeInfo::SetHasValue) - .property("arraySize", OptionalGetter(&symbols_backend::api::TypeInfo::GetArraySize), OptionalSetter(&symbols_backend::api::TypeInfo::SetArraySize)) + .property("arraySize", &symbols_backend::api::TypeInfo::GetArraySize, &symbols_backend::api::TypeInfo::SetArraySize) .property("isPointer", &symbols_backend::api::TypeInfo::GetIsPointer, &symbols_backend::api::TypeInfo::SetIsPointer) .property("members", &symbols_backend::api::TypeInfo::GetMembers, &symbols_backend::api::TypeInfo::SetMembers) .property("enumerators", &symbols_backend::api::TypeInfo::GetEnumerators, &symbols_backend::api::TypeInfo::SetEnumerators) + .property("extendedInfo", OptionalGetter(&symbols_backend::api::TypeInfo::GetExtendedInfo), OptionalSetter(&symbols_backend::api::TypeInfo::SetExtendedInfo)) ; emscripten::class_("AddRawModuleResponse") @@ -209,7 +236,7 @@ EMSCRIPTEN_BINDINGS(DWARFSymbolsPlugin) { emscripten::class_("GetMappedLinesResponse") .constructor<>() - .property("MappedLines", &symbols_backend::api::GetMappedLinesResponse::GetMappedLines, &symbols_backend::api::GetMappedLinesResponse::SetMappedLines) + .property("mappedLines", &symbols_backend::api::GetMappedLinesResponse::GetMappedLines, &symbols_backend::api::GetMappedLinesResponse::SetMappedLines) .property("error", OptionalGetter(&symbols_backend::api::GetMappedLinesResponse::GetError), OptionalSetter(&symbols_backend::api::GetMappedLinesResponse::SetError)) ; @@ -231,6 +258,9 @@ EMSCRIPTEN_BINDINGS(DWARFSymbolsPlugin) { emscripten::register_vector("TypeInfoArray"); emscripten::register_vector("FieldInfoArray"); emscripten::register_vector("EnumeratorArray"); + emscripten::register_vector("VariantPartInfoArray"); + emscripten::register_vector("VariantInfoArray"); + emscripten::register_vector("TemplateParameterInfoArray"); emscripten::register_vector("ByteArray"); emscripten::class_("DWARFSymbolsPluginBase") diff --git a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts index 8190551..7987fa1 100644 --- a/wasm/symbols-backend/tests/LLDBEvalTests.d.ts +++ b/wasm/symbols-backend/tests/LLDBEvalTests.d.ts @@ -1,7 +1,8 @@ // Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the wasm/symbols-backend/LICENSE file. -import type { Vector } from '../../../src/SymbolsBackend'; + +import type { Vector } from '../../../src/embind'; export interface Debugger { runToLine(line: string): void; From 14511ec6e6da2ec88375cc4c6fc581f62a034e52 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Thu, 1 May 2025 13:00:38 +1000 Subject: [PATCH 7/9] Remove `only: true` from wasm/e2e/rust_app.yaml --- wasm/e2e/rust_app.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/wasm/e2e/rust_app.yaml b/wasm/e2e/rust_app.yaml index ee4250e..7e17fab 100644 --- a/wasm/e2e/rust_app.yaml +++ b/wasm/e2e/rust_app.yaml @@ -2,7 +2,6 @@ name: Rust application source_file: resources/app.rs flags: [ [--target, wasm32-wasip1, -g]] -only: true script: - reason: setup actions: From f5fc47e6599d479b317957a4560dcdf2d5f91d82 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Thu, 8 May 2025 18:54:01 +1000 Subject: [PATCH 8/9] Add formatters for Rust collection types VecDeque, HashMap and HashSet --- README.md | 14 +++++++-- src/Formatters.ts | 56 ++++++++++++++++++++++++++++++++++ test-utils/TestValue.ts | 10 +++++- test/Formatters.test.ts | 61 +++++++++++++++++++++++++++++++++++++ test/SymbolsBackend.test.ts | 4 +-- test/e2e/e2e-specs.test.ts | 40 +++++++++++++++++++++--- wasm/e2e/resources/app.rs | 12 ++++++++ wasm/e2e/rust_app.yaml | 54 +++++++++++++++++++++++--------- 8 files changed, 226 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ba59bf7..d050505 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,17 @@ This project works by compiling the WebAssembly backend for the Chrome [C/C++ De In addition for the variable view and the basic `C/C++` expression evaluation support via [lldb-eval](https://github.com/google/lldb-eval) included in the original Chrome C/C++ Debugging Extension the following features has been added: - - Basic support for `Rust` types, most specifically sum types which couldn't be viewed in the original extension but also better support for core library types like `Vec` and `String` which were partially viewable in the original extension. - - + - Basic support for `Rust` types, most specifically sum types which couldn't be viewed in the original extension but also better support for core / standard library types like: + - `&[T]` + - `alloc::vec::Vec` + - `alloc::collections::vec_deque::VecDeque` + - `&str` + - `alloc::string::String` + - `alloc::rc::Rc` + - `alloc::rc::Weak` + - `std::collections::hash::map::HashMap` + - `std::collections::hash::map::HashSet` + ## Contributing ### Building (using Docker or Podman) diff --git a/src/Formatters.ts b/src/Formatters.ts index 08b5587..6df699a 100644 --- a/src/Formatters.ts +++ b/src/Formatters.ts @@ -397,6 +397,22 @@ export function formatRustVector(wasm: WasmInterface, value: Value) { return elements; } +export function formatRustVectorDeque(wasm: WasmInterface, value: Value) { + const elementTypeId = value.extendedTypeInfo?.templateParameters[0]?.typeId; + if (!elementTypeId) { + throw new Error(`Can't determine element type`); + } + const data = value.$('buf.inner.ptr').asPointerToType(elementTypeId); + const capacity = value.$('buf.inner.cap').asUint32(); + const head = value.$('head').asUint32(); + const size = value.$('len').asUint32(); + const elements: Value[] = []; + for (let i = 0; i < size; ++i) { + elements.push(data.$((head + i) % capacity)); + } + return elements; +} + export function formatRustStringSlice(wasm: WasmInterface, value: Value) { const data = value.$('data_ptr').asUint32(); const size = value.$('length').asUint32(); @@ -428,6 +444,41 @@ export function formatRustRcPointer(wasm: WasmInterface, value: Value): { [key: }; } +export function formatRustHashMap(wasm: WasmInterface, value: Value): { key: Value; value: Value; }[] { + return formatRustHashMapBase(wasm, value.$('base')); +} + +export function formatRustHashMapBase(wasm: WasmInterface, value: Value): { key: Value; value: Value; }[] { + const rawTable = value.$('table'); + const elementTypeId = rawTable.extendedTypeInfo?.templateParameters[0]?.typeId; + if (!elementTypeId) { + throw new Error(`Can't determine element type`); + } + const rawTableInner = rawTable.$('table'); + const bucketMask = rawTableInner.$('bucket_mask').asUint32(); + const dataPointer = rawTableInner.$('ctrl.pointer').asPointerToType(elementTypeId); + const tagsView = rawTableInner.$('ctrl.pointer.*').asDataView(0, bucketMask + 1); + const elements: { key: Value; value: Value; }[] = []; + for (let i = 0; i <= bucketMask; i++) { + if ((tagsView.getUint8(i) & 0x80) === 0) { + const keyValuePair = dataPointer.$(-(i + 1)); + elements.push({ + key: keyValuePair.$('__0'), + value: keyValuePair.$('__1'), + }); + } + } + return elements; +} + +export function formatRustHashSet(wasm: WasmInterface, value: Value): Value[] { + return formatRustHashSetBase(wasm, value.$('base')); +} + +export function formatRustHashSetBase(wasm: WasmInterface, value: Value): Value[] { + return formatRustHashMapBase(wasm, value.$('map')).map(element => element.key); +} + function getDiscriminatorValue(value: Value, discriminatorMember: FieldInfo) { const discriminatorValue = value.asType(discriminatorMember.typeId, discriminatorMember.offset); switch (discriminatorValue.size) { @@ -447,6 +498,11 @@ CustomFormatters.addFormatter({ types: rustTypeMatch(hasVariantParts), format: f CustomFormatters.addFormatter({ types: rustTypeMatch(hasOnlyRustTupleLikeMembers), format: formatRustTuple }); CustomFormatters.addFormatter({ types: rustTypeMatch(/^&\[.+\]$/), format: formatRustArraySlice }); CustomFormatters.addFormatter({ types: rustTypeMatch(/^alloc::vec::Vec<.+>$/), format: formatRustVector }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^alloc::collections::vec_deque::VecDeque<.+>$/), format: formatRustVectorDeque }); CustomFormatters.addFormatter({ types: rustTypeMatch('&str'), format: formatRustStringSlice }); CustomFormatters.addFormatter({ types: rustTypeMatch('alloc::string::String'), format: formatRustString }); CustomFormatters.addFormatter({ types: rustTypeMatch(/^alloc::rc::(Rc|Weak)<.+>$/), format: formatRustRcPointer }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^std::collections::hash::map::HashMap<.+>$/), format: formatRustHashMap }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^hashbrown::map::HashMap<.+>$/), format: formatRustHashMapBase }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^std::collections::hash::set::HashSet<.+>$/), format: formatRustHashSet }); +CustomFormatters.addFormatter({ types: rustTypeMatch(/^hashbrown::set::HashSet<.+>$/), format: formatRustHashSetBase }); diff --git a/test-utils/TestValue.ts b/test-utils/TestValue.ts index 1c112d2..7d727f2 100644 --- a/test-utils/TestValue.ts +++ b/test-utils/TestValue.ts @@ -85,7 +85,15 @@ export default class TestValue implements Value { return new TestValue(content, `${elements[0].typeNames[0]}${space}*`, members); } static fromType(typeName: string, data: TypedArrayLike = new Uint8Array(), location?: number) { - return new TestValue(new DataView(data.buffer), typeName, undefined, location); + let members: undefined | { [key: string]: TestValue, [key: number]: TestValue; }; + if (typeName.startsWith('(') && typeName.endsWith(')')) { + members = Object.fromEntries( + typeName.slice(1, typeName.length - 1) + .split(',') + .map((s, i) => [`__${i}`, TestValue.fromType(s.trim())]), + ); + } + return new TestValue(new DataView(data.buffer), typeName, members, location); } static fromMembers(typeName: string, members: { [key: string]: TestValue, [key: number]: TestValue; }): TestValue { return new TestValue(new DataView(new ArrayBuffer(0)), typeName, members); diff --git a/test/Formatters.test.ts b/test/Formatters.test.ts index d0686f2..2110eae 100644 --- a/test/Formatters.test.ts +++ b/test/Formatters.test.ts @@ -400,6 +400,63 @@ describe('Formatters', () => { } ); }); + + it('formatRustHashMap', async () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('std::collections::hash::map::HashMap', { + base: TestValue.fromMembers('hashbrown::map::HashMap', { + table: TestValue.fromMembers('', { + table: TestValue.fromMembers('', { + bucket_mask: TestValue.fromUint32(3), + ctrl: TestValue.fromMembers('', { + pointer: TestValue.pointerTo([TestValue.fromUint64(0xFF_7F_7F_FFn)]) + }) + }) + }).withTemplateParameters('(test::Key, test::Value)') + }) + }); + + assert.deepEqual( + Formatters.formatRustHashMap(wasm, value), + [ + { + key: TestValue.fromType('test::Key'), + value: TestValue.fromType('test::Value') + }, + { + key: TestValue.fromType('test::Key'), + value: TestValue.fromType('test::Value') + } + ] + ); + }); + + it('formatRustHashSet', async () => { + const wasm = new TestWasmInterface(); + const value = TestValue.fromMembers('std::collections::hash::set::HashSet', { + base: TestValue.fromMembers('hashbrown::map::HashSet', { + map: TestValue.fromMembers('hashbrown::map::HashMap', { + table: TestValue.fromMembers('', { + table: TestValue.fromMembers('', { + bucket_mask: TestValue.fromUint32(7), + ctrl: TestValue.fromMembers('', { + pointer: TestValue.pointerTo([TestValue.fromUint64(0x7F_FF_FF_FF_7F_7F_FF_FFn)]) + }) + }) + }).withTemplateParameters('(test::Value, ())') + }) + }) + }); + + assert.deepEqual( + Formatters.formatRustHashSet(wasm, value), + [ + TestValue.fromType('test::Value'), + TestValue.fromType('test::Value'), + TestValue.fromType('test::Value') + ] + ); + }); }); }); @@ -533,5 +590,9 @@ describe('CustomFormatters', () => { assert.strictEqual(CustomFormatters.get(type('&str'))?.format, Formatters.formatRustStringSlice); assert.strictEqual(CustomFormatters.get(type('alloc::string::String'))?.format, Formatters.formatRustString); assert.strictEqual(CustomFormatters.get(type('alloc::rc::Rc'))?.format, Formatters.formatRustRcPointer); + assert.strictEqual(CustomFormatters.get(type('std::collections::hash::map::HashMap'))?.format, Formatters.formatRustHashMap); + assert.strictEqual(CustomFormatters.get(type('hashbrown::map::HashMap'))?.format, Formatters.formatRustHashMapBase); + assert.strictEqual(CustomFormatters.get(type('std::collections::hash::set::HashSet'))?.format, Formatters.formatRustHashSet); + assert.strictEqual(CustomFormatters.get(type('hashbrown::set::HashSet'))?.format, Formatters.formatRustHashSetBase); }); }); diff --git a/test/SymbolsBackend.test.ts b/test/SymbolsBackend.test.ts index 3070506..f6ab317 100644 --- a/test/SymbolsBackend.test.ts +++ b/test/SymbolsBackend.test.ts @@ -46,7 +46,7 @@ describe('SymbolsBackend', () => { try { okReponse(plugin.AddRawModule('1', 'app_Rust_application_0.wasm')); - const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 31, -1)); + const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 32, -1)); const debuggerProxy = new TestWasmInterface(); debuggerProxy.memory = new SharedArrayBuffer(32); @@ -87,7 +87,7 @@ describe('SymbolsBackend', () => { try { okReponse(plugin.AddRawModule('1', 'app_Rust_application_0.wasm')); - const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 35, -1)); + const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 38, -1)); const debuggerProxy = new TestWasmInterface(); debuggerProxy.memory = new SharedArrayBuffer(32); diff --git a/test/e2e/e2e-specs.test.ts b/test/e2e/e2e-specs.test.ts index 50b74f7..01c99e2 100644 --- a/test/e2e/e2e-specs.test.ts +++ b/test/e2e/e2e-specs.test.ts @@ -31,6 +31,10 @@ interface TestSpec { name: string; type?: string; value?: unknown; + has_element?: { + name: string; + value: unknown; + }[]; }[]; evaluations?: { expression: string; @@ -133,11 +137,18 @@ async function defineTestCaseScript(test: TestSpec, testCallback: (description: isResumed = false; }); - for (const { name, type, value } of variables || []) { - testCallback({ variable: name, type, value }, async (debuggerSession) => { + for (const { name, type, value, has_element } of variables || []) { + testCallback({ variable: name, type, value, has_element: has_element?.map(m => `(${m.name} = ${m.value})`).join(' && ') }, async (debuggerSession) => { const result = await getVariable(debuggerSession, name); assert(value === undefined || result.value === `${value}` || result.description === `${value}`, `Actual value: ${result.value}`); assert(type === undefined || result.description === type, `Actual type: '${result.description}'`); + if (has_element) { + const elements = await getVariableElements(debuggerSession, name, has_element.map(element => element.name)); + assert( + elements.some(actualValues => has_element.every(({ name, value }) => actualValues[name] === `${value}`)), + `Actual elements:${elements.map(Object.entries).map(entries => `\n ${entries.map(([name, value]) => `(${name} = ${value})`).join(' && ')}`).join('')}`, + ); + } }); } @@ -228,7 +239,26 @@ async function getVariable(debuggerSession: DebuggerSession, scopedPath: string) return formatEvaluateResult(value); } -function formatEvaluateResult(result: Chrome.DevTools.RemoteObject | Chrome.DevTools.ForeignObject | null): { value: string, description?: string; } { +async function getVariableElements(debuggerSession: DebuggerSession, scopedPath: string, subPaths: string[]) { + const { objectId } = await getVariable(debuggerSession, scopedPath); + assert(objectId, `Expected variable ${scopedPath} to be an object.`); + const elementProperties = await debuggerSession.getProperties(objectId); + const elementValues = elementProperties.map(() => ({} as Record)); + for (let i = 0; i < elementValues.length; i++) { + for (const path of subPaths) { + const { value } = await getVariable(debuggerSession, `${scopedPath}.${elementProperties[i].name}.${path}`); + elementValues[i][path] = value; + } + } + return elementValues; +} + +function formatEvaluateResult(result: Chrome.DevTools.RemoteObject | Chrome.DevTools.ForeignObject | null): { + value: string; + description?: string; + objectId?: Chrome.DevTools.RemoteObjectId; + type?: Chrome.DevTools.RemoteObjectType; +} { if (result == null) { return { value: 'null' }; } @@ -240,7 +270,9 @@ function formatEvaluateResult(result: Chrome.DevTools.RemoteObject | Chrome.DevT } return { value: `${result.value}`, - description: result.description + description: result.description, + objectId: result.objectId, + type: result.type, }; } diff --git a/wasm/e2e/resources/app.rs b/wasm/e2e/resources/app.rs index dbe49dd..278bd42 100644 --- a/wasm/e2e/resources/app.rs +++ b/wasm/e2e/resources/app.rs @@ -1,3 +1,5 @@ +use std::collections::{HashMap, VecDeque}; + enum Animal { Dog(String, f64), Cat { name: String, weight: f64 }, @@ -31,11 +33,21 @@ fn main() { print_animal_info(a); print_animal_info(b); + let hash_map = HashMap::from([(1, "One"), (2, "Two")]); let vector = Vec::from([1, 2, 3, 4, 5]); + let mut vec_deque = VecDeque::from([1, 2, 3, 4, 5]); let vector_slice = &vector[1..3]; let string_slice = &"Biscuit".to_string()[2..5]; + vec_deque.pop_front(); + vec_deque.pop_front(); + vec_deque.pop_front(); + vec_deque.push_back(1); + vec_deque.push_back(2); + + println!("hash_map: {:?}", hash_map); println!("vector: {:?}", vector); + println!("vec_deque: {:?}", vec_deque); println!("vector_slice: {:?}", vector_slice); println!("string_slice: {:?}", string_slice); diff --git a/wasm/e2e/rust_app.yaml b/wasm/e2e/rust_app.yaml index 7e17fab..5dbab08 100644 --- a/wasm/e2e/rust_app.yaml +++ b/wasm/e2e/rust_app.yaml @@ -7,28 +7,28 @@ script: actions: - file: app.rs action: set_breakpoint - breakpoint: 14 + breakpoint: 16 - file: app.rs action: set_breakpoint - breakpoint: 16 + breakpoint: 18 - file: app.rs action: set_breakpoint - breakpoint: 19 + breakpoint: 21 - file: app.rs action: set_breakpoint - breakpoint: 38 + breakpoint: 48 - file: app.rs action: set_breakpoint - breakpoint: 46 + breakpoint: 58 - file: app.rs action: set_breakpoint - breakpoint: 54 + breakpoint: 66 - file: app.rs action: set_breakpoint - breakpoint: 61 + breakpoint: 73 - reason: breakpoint file: app.rs - line: 14 + line: 16 variables: - name: Parameter.animal type: app::Animal @@ -40,7 +40,7 @@ script: value: 8.51 - reason: breakpoint file: app.rs - line: 16 + line: 18 variables: - name: Local.name.string value: '"Biscuit"' @@ -48,7 +48,7 @@ script: value: 8.51 - reason: breakpoint file: app.rs - line: 14 + line: 16 variables: - name: Parameter.animal type: app::Animal @@ -60,7 +60,7 @@ script: value: 3.15 - reason: breakpoint file: app.rs - line: 19 + line: 21 variables: - name: Local.name.string value: '"Whiskers"' @@ -68,8 +68,22 @@ script: value: 3.15 - reason: breakpoint file: app.rs - line: 38 + line: 48 variables: + - name: Local.hash_map + type: 'std::collections::hash::map::HashMap' + - name: Local.hash_map + has_element: + - name: key + value: 1 + - name: value.string + value: '"One"' + - name: Local.hash_map + has_element: + - name: key + value: 2 + - name: value.string + value: '"Two"' - name: Local.vector type: 'alloc::vec::Vec' - name: Local.vector.0 @@ -82,6 +96,16 @@ script: value: 4 - name: Local.vector.4 value: 5 + - name: Local.vec_deque + type: 'alloc::collections::vec_deque::VecDeque' + - name: Local.vec_deque.0 + value: 4 + - name: Local.vec_deque.1 + value: 5 + - name: Local.vec_deque.2 + value: 1 + - name: Local.vec_deque.3 + value: 2 - name: Local.vector_slice type: '&[i32]' - name: Local.vector_slice.0 @@ -96,7 +120,7 @@ script: value: '"scu"' - reason: breakpoint file: app.rs - line: 46 + line: 58 variables: - name: Local.strong_ref.$0 type: app::Point2D @@ -116,7 +140,7 @@ script: value: 2 - reason: breakpoint file: app.rs - line: 54 + line: 66 variables: - name: Local.weak_ref_2.strong_count value: 1 @@ -124,7 +148,7 @@ script: value: 1 - reason: breakpoint file: app.rs - line: 61 + line: 73 variables: - name: Local.weak_ref_2.$0 value: null \ No newline at end of file From 8d65464c40096e6669bcb82c5293107db9e4c906 Mon Sep 17 00:00:00 2001 From: Mikael Waltersson Date: Wed, 7 Jan 2026 10:23:15 +1300 Subject: [PATCH 9/9] Fix issue with `SymbolsBackend::GetMappedLines()` returning invalid `-1` values for symbol context without line entries, causing exceptions downstream as `-1` gets interpreted as the Uint32 value `0xFFFFFFFF` in `vscode-js-debug`. Also fixed `test-utils/sync-interface/worker.js` mixed `import` / `require` statements not working with newer versions of nodejs and flaky `test/SymbolsBackend.test.ts` --- test-utils/sync-interface/worker.js | 2 +- test/SymbolsBackend.test.ts | 24 +++++++++++------------- wasm/symbols-backend/lib/WasmModule.cc | 5 +++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/test-utils/sync-interface/worker.js b/test-utils/sync-interface/worker.js index 82a6526..a57f5ac 100644 --- a/test-utils/sync-interface/worker.js +++ b/test-utils/sync-interface/worker.js @@ -1,4 +1,4 @@ -import { parentPort } from 'worker_threads'; +const { parentPort } = require('worker_threads'); let signal, commandPort, instance; diff --git a/test/SymbolsBackend.test.ts b/test/SymbolsBackend.test.ts index f6ab317..0b6a62d 100644 --- a/test/SymbolsBackend.test.ts +++ b/test/SymbolsBackend.test.ts @@ -48,15 +48,11 @@ describe('SymbolsBackend', () => { const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 32, -1)); - const debuggerProxy = new TestWasmInterface(); - debuggerProxy.memory = new SharedArrayBuffer(32); - debuggerProxy.locals.set(2, { type: 'i32', value: 4 }); - const { root: animalType, typeInfos } = okReponse( plugin.EvaluateExpression( { rawModuleId: '1', codeOffset, inlineFrameIndex: 0 }, 'a', - debuggerProxy, + createDebuggerProxyWasmInterface(), ) ); @@ -87,17 +83,12 @@ describe('SymbolsBackend', () => { try { okReponse(plugin.AddRawModule('1', 'app_Rust_application_0.wasm')); - const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 38, -1)); - - const debuggerProxy = new TestWasmInterface(); - debuggerProxy.memory = new SharedArrayBuffer(32); - debuggerProxy.locals.set(2, { type: 'i32', value: 4 }); - + const { rawLocationRanges: [{ startOffset: codeOffset }] } = okReponse(plugin.SourceLocationToRawLocation('1', 'app.rs', 37, -1)); const { root: vectorType, typeInfos } = okReponse( plugin.EvaluateExpression( { rawModuleId: '1', codeOffset, inlineFrameIndex: 0 }, 'vector', - debuggerProxy, + createDebuggerProxyWasmInterface(), ) ); @@ -127,4 +118,11 @@ function okReponse(response: T) { assert.fail(`Expect succesfull response, got ${response.error.code}: ${response.error.message}`); } return response; -} \ No newline at end of file +} + +function createDebuggerProxyWasmInterface(memorySize = 4096, numberOfLocals = 32) { + return Object.assign(new TestWasmInterface(), { + memory: new SharedArrayBuffer(memorySize), + locals: new Map(Array.from({ length: numberOfLocals }).map((_, i) => [i, { type: 'i32', value: 0 }])), + }); +} diff --git a/wasm/symbols-backend/lib/WasmModule.cc b/wasm/symbols-backend/lib/WasmModule.cc index 570dca2..4ea5399 100644 --- a/wasm/symbols-backend/lib/WasmModule.cc +++ b/wasm/symbols-backend/lib/WasmModule.cc @@ -308,8 +308,9 @@ std::vector WasmModule::GetMappedLines( std::vector line_numbers; for (const lldb_private::SymbolContext& line_sc : line_entry_scs.SymbolContexts()) { - assert(line_sc.line_entry.IsValid()); - line_numbers.push_back(line_sc.line_entry.line); + if (line_sc.line_entry.IsValid() && line_sc.line_entry.line > 0) { + line_numbers.push_back(line_sc.line_entry.line); + } } std::sort(line_numbers.begin(), line_numbers.end());