aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gdb/Makefile.in1
-rw-r--r--gdb/NEWS4
-rw-r--r--gdb/data-directory/Makefile.in16
-rw-r--r--gdb/doc/gdb.texinfo9
-rw-r--r--gdb/python/lib/gdb/dap/__init__.py69
-rw-r--r--gdb/python/lib/gdb/dap/breakpoint.py143
-rw-r--r--gdb/python/lib/gdb/dap/bt.py93
-rw-r--r--gdb/python/lib/gdb/dap/disassemble.py51
-rw-r--r--gdb/python/lib/gdb/dap/evaluate.py42
-rw-r--r--gdb/python/lib/gdb/dap/events.py166
-rw-r--r--gdb/python/lib/gdb/dap/frames.py57
-rw-r--r--gdb/python/lib/gdb/dap/io.py67
-rw-r--r--gdb/python/lib/gdb/dap/launch.py39
-rw-r--r--gdb/python/lib/gdb/dap/next.py51
-rw-r--r--gdb/python/lib/gdb/dap/pause.py23
-rw-r--r--gdb/python/lib/gdb/dap/scopes.py65
-rw-r--r--gdb/python/lib/gdb/dap/server.py205
-rw-r--r--gdb/python/lib/gdb/dap/startup.py189
-rw-r--r--gdb/python/lib/gdb/dap/state.py25
-rw-r--r--gdb/python/lib/gdb/dap/threads.py42
-rw-r--r--gdb/python/py-dap.c99
-rw-r--r--gdb/testsuite/gdb.dap/basic-dap.c44
-rw-r--r--gdb/testsuite/gdb.dap/basic-dap.exp151
-rw-r--r--gdb/testsuite/lib/dap-support.exp343
-rw-r--r--gdb/testsuite/lib/mi-support.exp2
-rw-r--r--gdb/testsuite/lib/ton.tcl303
26 files changed, 2297 insertions, 2 deletions
diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index c5d66e4..b22a6c6 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -396,6 +396,7 @@ SUBDIR_PYTHON_SRCS = \
python/py-cmd.c \
python/py-connection.c \
python/py-continueevent.c \
+ python/py-dap.c \
python/py-disasm.c \
python/py-event.c \
python/py-evtregistry.c \
diff --git a/gdb/NEWS b/gdb/NEWS
index e61f060..41d8155 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -5,6 +5,10 @@
* MI version 1 has been removed.
+* GDB has initial built-in support for the Debugger Adapter Protocol.
+ This support requires that GDB be built with Python scripting
+ enabled.
+
*** Changes in GDB 13
* MI version 1 is deprecated, and will be removed in GDB 14.
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index 557a63b..f113929 100644
--- a/gdb/data-directory/Makefile.in
+++ b/gdb/data-directory/Makefile.in
@@ -87,6 +87,22 @@ PYTHON_FILE_LIST = \
gdb/command/type_printers.py \
gdb/command/unwinders.py \
gdb/command/xmethods.py \
+ gdb/dap/breakpoint.py \
+ gdb/dap/bt.py \
+ gdb/dap/disassemble.py \
+ gdb/dap/evaluate.py \
+ gdb/dap/events.py \
+ gdb/dap/frames.py \
+ gdb/dap/__init__.py \
+ gdb/dap/io.py \
+ gdb/dap/launch.py \
+ gdb/dap/next.py \
+ gdb/dap/pause.py \
+ gdb/dap/scopes.py \
+ gdb/dap/server.py \
+ gdb/dap/startup.py \
+ gdb/dap/state.py \
+ gdb/dap/threads.py \
gdb/function/__init__.py \
gdb/function/as_string.py \
gdb/function/caller_is.py \
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index a72b2b9..ea54f25 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -29136,6 +29136,15 @@ The traditional console or command-line interpreter. This is the most often
used interpreter with @value{GDBN}. With no interpreter specified at runtime,
@value{GDBN} will use this interpreter.
+@item dap
+@cindex DAP
+@cindex Debugger Adapter Protocol
+When @value{GDBN} has been built with Python support, it also supports
+the Debugger Adapter Protocol. This protocol can be used by a
+debugger GUI or an IDE to communicate with @value{GDBN}. This
+protocol is documented at
+@url{https://microsoft.github.io/debug-adapter-protocol/}.
+
@item mi
@cindex mi interpreter
The newest @sc{gdb/mi} interface (currently @code{mi3}). Used primarily
diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py
new file mode 100644
index 0000000..0df9386
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/__init__.py
@@ -0,0 +1,69 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import gdb
+
+# This must come before other DAP imports.
+from . import startup
+
+# Load modules that define commands.
+from . import breakpoint
+from . import bt
+from . import disassemble
+from . import evaluate
+from . import launch
+from . import next
+from . import pause
+from . import scopes
+from . import threads
+
+from .server import Server
+
+
+def run():
+ """Main entry point for the DAP server.
+ This is called by the GDB DAP interpreter."""
+ startup.exec_and_log("set python print-stack full")
+ startup.exec_and_log("set pagination off")
+
+ # We want to control gdb stdin and stdout entirely, so we dup
+ # them to new file descriptors.
+ saved_out = os.dup(1)
+ saved_in = os.dup(0)
+ # Make sure these are not inheritable. This is already the case
+ # for Unix, but not for Windows.
+ os.set_inheritable(saved_out, False)
+ os.set_inheritable(saved_in, False)
+
+ # The new gdb (and inferior) stdin will just be /dev/null. For
+ # gdb, the "dap" interpreter also rewires the UI so that gdb
+ # doesn't try to read this (and thus see EOF and exit).
+ new_in = os.open(os.devnull, os.O_RDONLY)
+ os.dup2(new_in, 0, True)
+ os.close(new_in)
+
+ # Make the new stdout be a pipe. This way the DAP code can easily
+ # read from the inferior and send OutputEvent to the client.
+ (rfd, wfd) = os.pipe()
+ os.set_inheritable(rfd, False)
+ os.dup2(wfd, 1, True)
+ # Also send stderr this way.
+ os.dup2(wfd, 2, True)
+ os.close(wfd)
+
+ # Note the inferior output is opened in text mode.
+ server = Server(open(saved_in, "rb"), open(saved_out, "wb"), open(rfd, "r"))
+ startup.start_dap(server.main_loop)
diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py
new file mode 100644
index 0000000..502beb0
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/breakpoint.py
@@ -0,0 +1,143 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Map from the breakpoint "kind" (like "function") to a second map, of
+# breakpoints of that type. The second map uses the breakpoint spec
+# as a key, and the gdb.Breakpoint itself as a value. This is used to
+# implement the clearing behavior specified by the protocol, while
+# allowing for reuse when a breakpoint can be kept.
+breakpoint_map = {}
+
+
+@in_gdb_thread
+def breakpoint_descriptor(bp):
+ "Return the Breakpoint object descriptor given a gdb Breakpoint."
+ if bp.locations:
+ # Just choose the first location, because DAP doesn't allow
+ # multiple locations. See
+ # https://github.com/microsoft/debug-adapter-protocol/issues/13
+ loc = bp.locations[0]
+ (basename, line) = loc.source
+ return {
+ "id": bp.number,
+ "verified": True,
+ "source": {
+ "name": os.path.basename(basename),
+ "path": loc.fullname,
+ # We probably don't need this but it doesn't hurt to
+ # be explicit.
+ "sourceReference": 0,
+ },
+ "line": line,
+ "instructionReference": hex(loc.address),
+ }
+ else:
+ return {
+ "id": bp.number,
+ "verified": False,
+ }
+
+
+# Helper function to set some breakpoints according to a list of
+# specifications.
+@in_gdb_thread
+def _set_breakpoints(kind, specs):
+ global breakpoint_map
+ # Try to reuse existing breakpoints if possible.
+ if kind in breakpoint_map:
+ saved_map = breakpoint_map[kind]
+ else:
+ saved_map = {}
+ breakpoint_map[kind] = {}
+ result = []
+ for spec in specs:
+ keyspec = frozenset(spec.items())
+ if keyspec in saved_map:
+ bp = saved_map.pop(keyspec)
+ else:
+ # FIXME handle exceptions here
+ bp = gdb.Breakpoint(**spec)
+ breakpoint_map[kind][keyspec] = bp
+ result.append(breakpoint_descriptor(bp))
+ # Delete any breakpoints that were not reused.
+ for entry in saved_map.values():
+ entry.delete()
+ return result
+
+
+@request("setBreakpoints")
+def set_breakpoint(source, *, breakpoints=[], **args):
+ if "path" not in source:
+ result = []
+ else:
+ specs = []
+ for obj in breakpoints:
+ specs.append(
+ {
+ "source": source["path"],
+ "line": obj["line"],
+ }
+ )
+ # Be sure to include the path in the key, so that we only
+ # clear out breakpoints coming from this same source.
+ key = "source:" + source["path"]
+ result = send_gdb_with_response(lambda: _set_breakpoints(key, specs))
+ return {
+ "breakpoints": result,
+ }
+
+
+@request("setFunctionBreakpoints")
+@capability("supportsFunctionBreakpoints")
+def set_fn_breakpoint(breakpoints, **args):
+ specs = []
+ for bp in breakpoints:
+ specs.append(
+ {
+ "function": bp["name"],
+ }
+ )
+ result = send_gdb_with_response(lambda: _set_breakpoints("function", specs))
+ return {
+ "breakpoints": result,
+ }
+
+
+@request("setInstructionBreakpoints")
+@capability("supportsInstructionBreakpoints")
+def set_insn_breakpoints(*, breakpoints, offset=None, **args):
+ specs = []
+ for bp in breakpoints:
+ # There's no way to set an explicit address breakpoint
+ # from Python, so we rely on "spec" instead.
+ val = "*" + bp["instructionReference"]
+ if offset is not None:
+ val = val + " + " + str(offset)
+ specs.append(
+ {
+ "spec": val,
+ }
+ )
+ result = send_gdb_with_response(lambda: _set_breakpoints("instruction", specs))
+ return {
+ "breakpoints": result,
+ }
diff --git a/gdb/python/lib/gdb/dap/bt.py b/gdb/python/lib/gdb/dap/bt.py
new file mode 100644
index 0000000..990ab13
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/bt.py
@@ -0,0 +1,93 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .frames import frame_id
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+from .state import set_thread
+
+
+# Helper function to safely get the name of a frame as a string.
+@in_gdb_thread
+def _frame_name(frame):
+ name = frame.name()
+ if name is None:
+ name = "???"
+ return name
+
+
+# Helper function to get a frame's SAL without an error.
+@in_gdb_thread
+def _safe_sal(frame):
+ try:
+ return frame.find_sal()
+ except gdb.error:
+ return None
+
+
+# Helper function to compute a stack trace.
+@in_gdb_thread
+def _backtrace(thread_id, levels, startFrame):
+ set_thread(thread_id)
+ frames = []
+ current_number = 0
+ # FIXME could invoke frame filters here.
+ try:
+ current_frame = gdb.newest_frame()
+ except gdb.error:
+ current_frame = None
+ # Note that we always iterate over all frames, which is lame, but
+ # seemingly necessary to support the totalFrames response.
+ # FIXME maybe the mildly mysterious note about "monotonically
+ # increasing totalFrames values" would let us fix this.
+ while current_frame is not None:
+ # This condition handles the startFrame==0 case as well.
+ if current_number >= startFrame and (levels == 0 or len(frames) < levels):
+ newframe = {
+ "id": frame_id(current_frame),
+ "name": _frame_name(current_frame),
+ # This must always be supplied, but we will set it
+ # correctly later if that is possible.
+ "line": 0,
+ # GDB doesn't support columns.
+ "column": 0,
+ "instructionPointerReference": hex(current_frame.pc()),
+ }
+ sal = _safe_sal(current_frame)
+ if sal is not None:
+ newframe["source"] = {
+ "name": os.path.basename(sal.symtab.filename),
+ "path": sal.symtab.filename,
+ # We probably don't need this but it doesn't hurt
+ # to be explicit.
+ "sourceReference": 0,
+ }
+ newframe["line"] = sal.line
+ frames.append(newframe)
+ current_number = current_number + 1
+ current_frame = current_frame.older()
+ return {
+ "stackFrames": frames,
+ "totalFrames": current_number,
+ }
+
+
+@request("stackTrace")
+@capability("supportsDelayedStackTraceLoading")
+def stacktrace(*, levels=0, startFrame=0, threadId, **extra):
+ return send_gdb_with_response(lambda: _backtrace(threadId, levels, startFrame))
diff --git a/gdb/python/lib/gdb/dap/disassemble.py b/gdb/python/lib/gdb/dap/disassemble.py
new file mode 100644
index 0000000..3d3b3a5
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/disassemble.py
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+@in_gdb_thread
+def _disassemble(pc, skip_insns, count):
+ try:
+ arch = gdb.selected_frame().architecture()
+ except gdb.error:
+ # Maybe there was no frame.
+ arch = gdb.selected_inferior().architecture()
+ result = []
+ total_count = skip_insns + count
+ for elt in arch.disassemble(pc, count=total_count)[skip_insns:]:
+ result.append(
+ {
+ "address": hex(elt["addr"]),
+ "instruction": elt["asm"],
+ }
+ )
+ return {
+ "instructions": result,
+ }
+
+
+@request("disassemble")
+@capability("supportsDisassembleRequest")
+def disassemble(
+ *, memoryReference, offset=0, instructionOffset=0, instructionCount, **extra
+):
+ pc = int(memoryReference, 0) + offset
+ return send_gdb_with_response(
+ lambda: _disassemble(pc, instructionOffset, instructionCount)
+ )
diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py
new file mode 100644
index 0000000..c05e62d
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/evaluate.py
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Helper function to evaluate an expression in a certain frame.
+@in_gdb_thread
+def _evaluate(expr, frame_id):
+ if frame_id is not None:
+ frame = frame_for_id(frame_id)
+ frame.select()
+ return str(gdb.parse_and_eval(expr))
+
+
+# FIXME 'format' & hex
+# FIXME return a structured response using pretty-printers / varobj
+# FIXME supportsVariableType handling
+@request("evaluate")
+def eval_request(expression, *, frameId=None, **args):
+ result = send_gdb_with_response(lambda: _evaluate(expression, frameId))
+ return {
+ "result": result,
+ # FIXME
+ "variablesReference": -1,
+ }
diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py
new file mode 100644
index 0000000..45e2a1e
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/events.py
@@ -0,0 +1,166 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import enum
+import gdb
+
+from .server import send_event
+from .startup import in_gdb_thread, Invoker, log
+from .breakpoint import breakpoint_descriptor
+
+
+@in_gdb_thread
+def _on_exit(event):
+ code = 0
+ if hasattr(event, "exit_code"):
+ code = event.exit_code
+ send_event(
+ "exited",
+ {
+ "exitCode": code,
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_modified(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "changed",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_created(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "new",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_deleted(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "removed",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _new_thread(event):
+ send_event(
+ "thread",
+ {
+ "reason": "started",
+ "threadId": event.inferior_thread.global_num,
+ },
+ )
+
+
+_suppress_cont = False
+
+
+@in_gdb_thread
+def _cont(event):
+ global _suppress_cont
+ if _suppress_cont:
+ log("_suppress_cont case")
+ _suppress_cont = False
+ else:
+ send_event(
+ "continued",
+ {
+ "threadId": gdb.selected_thread().global_num,
+ "allThreadsContinued": True,
+ },
+ )
+
+
+class StopKinds(enum.Enum):
+ # The values here are chosen to follow the DAP spec.
+ STEP = "step"
+ BREAKPOINT = "breakpoint"
+ PAUSE = "pause"
+ EXCEPTION = "exception"
+
+
+_expected_stop = None
+
+
+@in_gdb_thread
+def expect_stop(reason):
+ """Indicate that a stop is expected, for the reason given."""
+ global _expected_stop
+ _expected_stop = reason
+
+
+# A wrapper for Invoker that also sets the expected stop.
+class ExecutionInvoker(Invoker):
+ """A subclass of Invoker that sets the expected stop.
+ Note that this assumes that the command will restart the inferior,
+ so it will also cause ContinuedEvents to be suppressed."""
+
+ def __init__(self, cmd, expected):
+ super().__init__(cmd)
+ self.expected = expected
+
+ @in_gdb_thread
+ def __call__(self):
+ expect_stop(self.expected)
+ global _suppress_cont
+ _suppress_cont = True
+ # FIXME if the call fails should we clear _suppress_cont?
+ super().__call__()
+
+
+@in_gdb_thread
+def _on_stop(event):
+ log("entering _on_stop: " + repr(event))
+ global _expected_stop
+ obj = {
+ "threadId": gdb.selected_thread().global_num,
+ # FIXME we don't support non-stop for now.
+ "allThreadsStopped": True,
+ }
+ if isinstance(event, gdb.BreakpointEvent):
+ # Ignore the expected stop, we hit a breakpoint instead.
+ # FIXME differentiate between 'breakpoint', 'function breakpoint',
+ # 'data breakpoint' and 'instruction breakpoint' here.
+ _expected_stop = StopKinds.BREAKPOINT
+ obj["hitBreakpointIds"] = [x.number for x in event.breakpoints]
+ elif _expected_stop is None:
+ # FIXME what is even correct here
+ _expected_stop = StopKinds.EXCEPTION
+ obj["reason"] = _expected_stop.value
+ _expected_stop = None
+ send_event("stopped", obj)
+
+
+gdb.events.stop.connect(_on_stop)
+gdb.events.exited.connect(_on_exit)
+gdb.events.breakpoint_created.connect(_bp_created)
+gdb.events.breakpoint_modified.connect(_bp_modified)
+gdb.events.breakpoint_deleted.connect(_bp_deleted)
+gdb.events.new_thread.connect(_new_thread)
+gdb.events.cont.connect(_cont)
diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py
new file mode 100644
index 0000000..a1c2689
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/frames.py
@@ -0,0 +1,57 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .startup import in_gdb_thread
+
+
+# Map from frame (thread,level) pair to frame ID numbers. Note we
+# can't use the frame itself here as it is not hashable.
+_frame_ids = {}
+
+# Map from frame ID number back to frames.
+_id_to_frame = {}
+
+
+# Clear all the frame IDs.
+@in_gdb_thread
+def _clear_frame_ids(evt):
+ global _frame_ids, _id_to_frame
+ _frame_ids = {}
+ _id_to_frame = {}
+
+
+# Clear the frame ID map whenever the inferior runs.
+gdb.events.cont.connect(_clear_frame_ids)
+
+
+@in_gdb_thread
+def frame_id(frame):
+ """Return the frame identifier for FRAME."""
+ global _frame_ids, _id_to_frame
+ pair = (gdb.selected_thread().global_num, frame.level)
+ if pair not in _frame_ids:
+ id = len(_frame_ids)
+ _frame_ids[pair] = id
+ _id_to_frame[id] = frame
+ return _frame_ids[pair]
+
+
+@in_gdb_thread
+def frame_for_id(id):
+ """Given a frame identifier ID, return the corresponding frame."""
+ global _id_to_frame
+ return _id_to_frame[id]
diff --git a/gdb/python/lib/gdb/dap/io.py b/gdb/python/lib/gdb/dap/io.py
new file mode 100644
index 0000000..656ac08
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/io.py
@@ -0,0 +1,67 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+from .startup import start_thread, send_gdb
+
+
+def read_json(stream):
+ """Read a JSON-RPC message from STREAM.
+ The decoded object is returned."""
+ # First read and parse the header.
+ content_length = None
+ while True:
+ line = stream.readline()
+ line = line.strip()
+ if line == b"":
+ break
+ if line.startswith(b"Content-Length:"):
+ line = line[15:].strip()
+ content_length = int(line)
+ data = bytes()
+ while len(data) < content_length:
+ new_data = stream.read(content_length - len(data))
+ data += new_data
+ result = json.loads(data)
+ return result
+
+
+def start_json_writer(stream, queue):
+ """Start the JSON writer thread.
+ It will read objects from QUEUE and write them to STREAM,
+ following the JSON-RPC protocol."""
+
+ def _json_writer():
+ seq = 1
+ while True:
+ obj = queue.get()
+ if obj is None:
+ # This is an exit request. The stream is already
+ # flushed, so all that's left to do is request an
+ # exit.
+ send_gdb("quit")
+ break
+ obj["seq"] = seq
+ seq = seq + 1
+ encoded = json.dumps(obj)
+ body_bytes = encoded.encode("utf-8")
+ header = f"Content-Length: {len(body_bytes)}\r\n\r\n"
+ header_bytes = header.encode("ASCII")
+ stream.write(header_bytes)
+ stream.write(body_bytes)
+ stream.flush()
+
+ start_thread("JSON writer", _json_writer)
diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py
new file mode 100644
index 0000000..7ac8177
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/launch.py
@@ -0,0 +1,39 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import ExecutionInvoker
+from .server import request, capability
+from .startup import send_gdb
+
+
+_program = None
+
+
+@request("launch")
+def launch(*, program=None, **args):
+ if program is not None:
+ global _program
+ _program = program
+ send_gdb(f"file {_program}")
+
+
+@capability("supportsConfigurationDoneRequest")
+@request("configurationDone")
+def config_done(**args):
+ global _program
+ if _program is not None:
+ # Suppress the continue event, but don't set any particular
+ # expected stop.
+ send_gdb(ExecutionInvoker("run", None))
diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py
new file mode 100644
index 0000000..726b659
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/next.py
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import capability, request
+from .startup import send_gdb
+from .state import set_thread
+
+
+# Helper function to set the current thread.
+def _handle_thread_step(threadId):
+ # Ensure we're going to step the correct thread.
+ send_gdb(lambda: set_thread(threadId))
+
+
+@request("next")
+def next(*, threadId, granularity="statement", **args):
+ _handle_thread_step(threadId)
+ cmd = "next"
+ if granularity == "instruction":
+ cmd += "i"
+ send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@capability("supportsSteppingGranularity")
+@request("stepIn")
+def stepIn(*, threadId, granularity="statement", **args):
+ _handle_thread_step(threadId)
+ cmd = "step"
+ if granularity == "instruction":
+ cmd += "i"
+ send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@request("continue")
+def continue_request(**args):
+ send_gdb(ExecutionInvoker("continue", None))
+ # FIXME Just ignore threadId for the time being, and assume all-stop.
+ return {"allThreadsContinued": True}
diff --git a/gdb/python/lib/gdb/dap/pause.py b/gdb/python/lib/gdb/dap/pause.py
new file mode 100644
index 0000000..74fdf48
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/pause.py
@@ -0,0 +1,23 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import request
+from .startup import send_gdb
+
+
+@request("pause")
+def pause(**args):
+ send_gdb(ExecutionInvoker("interrupt -a", StopKinds.PAUSE))
diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py
new file mode 100644
index 0000000..0c887db
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/scopes.py
@@ -0,0 +1,65 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .startup import send_gdb_with_response, in_gdb_thread
+from .server import request
+
+
+# Helper function to return a frame's block without error.
+@in_gdb_thread
+def _safe_block(frame):
+ try:
+ return frame.block()
+ except gdb.error:
+ return None
+
+
+# Helper function to return a list of variables of block, up to the
+# enclosing function.
+@in_gdb_thread
+def _block_vars(block):
+ result = []
+ while True:
+ result += list(block)
+ if block.function is not None:
+ break
+ block = block.superblock
+ return result
+
+
+# Helper function to create a DAP scopes for a given frame ID.
+@in_gdb_thread
+def _get_scope(id):
+ frame = frame_for_id(id)
+ block = _safe_block(frame)
+ scopes = []
+ if block is not None:
+ new_scope = {
+ # FIXME
+ "name": "Locals",
+ "expensive": False,
+ "namedVariables": len(_block_vars(block)),
+ }
+ scopes.append(new_scope)
+ return scopes
+
+
+@request("scopes")
+def scopes(*, frameId, **extra):
+ scopes = send_gdb_with_response(lambda: _get_scope(frameId))
+ return {"scopes": scopes}
diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py
new file mode 100644
index 0000000..d6fc0bd
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/server.py
@@ -0,0 +1,205 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import queue
+
+from .io import start_json_writer, read_json
+from .startup import (
+ in_dap_thread,
+ start_thread,
+ log,
+ log_stack,
+ send_gdb_with_response,
+)
+
+
+# Map capability names to values.
+_capabilities = {}
+
+# Map command names to callables.
+_commands = {}
+
+# The global server.
+_server = None
+
+
+class Server:
+ """The DAP server class."""
+
+ def __init__(self, in_stream, out_stream, child_stream):
+ self.in_stream = in_stream
+ self.out_stream = out_stream
+ self.child_stream = child_stream
+ self.delayed_events = []
+ # This queue accepts JSON objects that are then sent to the
+ # DAP client. Writing is done in a separate thread to avoid
+ # blocking the read loop.
+ self.write_queue = queue.SimpleQueue()
+ self.done = False
+ global _server
+ _server = self
+
+ # Treat PARAMS as a JSON-RPC request and perform its action.
+ # PARAMS is just a dictionary from the JSON.
+ @in_dap_thread
+ def _handle_command(self, params):
+ # We don't handle 'cancel' for now.
+ result = {
+ "request_seq": params["seq"],
+ "type": "response",
+ "command": params["command"],
+ }
+ try:
+ if "arguments" in params:
+ args = params["arguments"]
+ else:
+ args = {}
+ global _commands
+ body = _commands[params["command"]](**args)
+ if body is not None:
+ result["body"] = body
+ result["success"] = True
+ except BaseException as e:
+ log_stack()
+ result["success"] = False
+ result["message"] = str(e)
+ return result
+
+ # Read inferior output and sends OutputEvents to the client. It
+ # is run in its own thread.
+ def _read_inferior_output(self):
+ while True:
+ line = self.child_stream.readline()
+ self.send_event(
+ "output",
+ {
+ "category": "stdout",
+ "output": line,
+ },
+ )
+
+ # Send OBJ to the client, logging first if needed.
+ def _send_json(self, obj):
+ log("WROTE: <<<" + json.dumps(obj) + ">>>")
+ self.write_queue.put(obj)
+
+ # This must be run in the DAP thread, but we can't use
+ # @in_dap_thread here because the global isn't set until after
+ # this starts running. FIXME.
+ def main_loop(self):
+ """The main loop of the DAP server."""
+ # Before looping, start the thread that writes JSON to the
+ # client, and the thread that reads output from the inferior.
+ start_thread("output reader", self._read_inferior_output)
+ start_json_writer(self.out_stream, self.write_queue)
+ while not self.done:
+ cmd = read_json(self.in_stream)
+ log("READ: <<<" + json.dumps(cmd) + ">>>")
+ result = self._handle_command(cmd)
+ self._send_json(result)
+ events = self.delayed_events
+ self.delayed_events = []
+ for (event, body) in events:
+ self.send_event(event, body)
+ # Got the terminate request. This is handled by the
+ # JSON-writing thread, so that we can ensure that all
+ # responses are flushed to the client before exiting.
+ self.write_queue.put(None)
+
+ @in_dap_thread
+ def send_event_later(self, event, body=None):
+ """Send a DAP event back to the client, but only after the
+ current request has completed."""
+ self.delayed_events.append((event, body))
+
+ # Note that this does not need to be run in any particular thread,
+ # because it just creates an object and writes it to a thread-safe
+ # queue.
+ def send_event(self, event, body=None):
+ """Send an event to the DAP client.
+ EVENT is the name of the event, a string.
+ BODY is the body of the event, an arbitrary object."""
+ obj = {
+ "type": "event",
+ "event": event,
+ }
+ if body is not None:
+ obj["body"] = body
+ self._send_json(obj)
+
+ def shutdown(self):
+ """Request that the server shut down."""
+ # Just set a flag. This operation is complicated because we
+ # want to write the result of the request before exiting. See
+ # main_loop.
+ self.done = True
+
+
+def send_event(event, body):
+ """Send an event to the DAP client.
+ EVENT is the name of the event, a string.
+ BODY is the body of the event, an arbitrary object."""
+ global _server
+ _server.send_event(event, body)
+
+
+def request(name):
+ """A decorator that indicates that the wrapper function implements
+ the DAP request NAME."""
+
+ def wrap(func):
+ global _commands
+ _commands[name] = func
+ # All requests must run in the DAP thread.
+ return in_dap_thread(func)
+
+ return wrap
+
+
+def capability(name):
+ """A decorator that indicates that the wrapper function implements
+ the DAP capability NAME."""
+
+ def wrap(func):
+ global _capabilities
+ _capabilities[name] = True
+ return func
+
+ return wrap
+
+
+@request("initialize")
+def initialize(**args):
+ global _server, _capabilities
+ _server.config = args
+ _server.send_event_later("initialized")
+ return _capabilities.copy()
+
+
+@request("terminate")
+@capability("supportsTerminateRequest")
+def terminate(**args):
+ # We can ignore the result here, because we only really need to
+ # synchronize.
+ send_gdb_with_response("kill")
+
+
+@request("disconnect")
+@capability("supportTerminateDebuggee")
+def disconnect(*, terminateDebuggee=False, **args):
+ if terminateDebuggee:
+ terminate()
+ _server.shutdown()
diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py
new file mode 100644
index 0000000..acfdcb4
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/startup.py
@@ -0,0 +1,189 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Do not import other gdbdap modules here -- this module must come
+# first.
+import functools
+import gdb
+import queue
+import signal
+import threading
+import traceback
+from contextlib import contextmanager
+
+
+# The GDB thread, aka the main thread.
+_gdb_thread = threading.current_thread()
+
+
+# The DAP thread.
+_dap_thread = None
+
+
+@contextmanager
+def blocked_signals():
+ """A helper function that blocks and unblocks signals."""
+ if not hasattr(signal, "pthread_sigmask"):
+ yield
+ return
+
+ to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
+ signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
+ try:
+ yield None
+ finally:
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
+
+
+def start_thread(name, target, args=()):
+ """Start a new thread, invoking TARGET with *ARGS there.
+ This is a helper function that ensures that any GDB signals are
+ correctly blocked."""
+ # GDB requires that these be delivered to the gdb thread. We
+ # do this here to avoid any possible race with the creation of
+ # the new thread. The thread mask is inherited by new
+ # threads.
+ with blocked_signals():
+ result = threading.Thread(target=target, args=args, daemon=True)
+ result.start()
+ return result
+
+
+def start_dap(target):
+ """Start the DAP thread and invoke TARGET there."""
+ global _dap_thread
+ exec_and_log("set breakpoint pending on")
+ _dap_thread = start_thread("DAP", target)
+
+
+def in_gdb_thread(func):
+ """A decorator that asserts that FUNC must be run in the GDB thread."""
+
+ @functools.wraps(func)
+ def ensure_gdb_thread(*args, **kwargs):
+ assert threading.current_thread() is _gdb_thread
+ return func(*args, **kwargs)
+
+ return ensure_gdb_thread
+
+
+def in_dap_thread(func):
+ """A decorator that asserts that FUNC must be run in the DAP thread."""
+
+ @functools.wraps(func)
+ def ensure_dap_thread(*args, **kwargs):
+ assert threading.current_thread() is _dap_thread
+ return func(*args, **kwargs)
+
+ return ensure_dap_thread
+
+
+class LoggingParam(gdb.Parameter):
+ """Whether DAP logging is enabled."""
+
+ set_doc = "Set the DAP logging status."
+ show_doc = "Show the DAP logging status."
+
+ log_file = None
+
+ def __init__(self):
+ super().__init__(
+ "debug dap-log-file", gdb.COMMAND_MAINTENANCE, gdb.PARAM_OPTIONAL_FILENAME
+ )
+ self.value = None
+
+ def get_set_string(self):
+ # Close any existing log file, no matter what.
+ if self.log_file is not None:
+ self.log_file.close()
+ self.log_file = None
+ if self.value is not None:
+ self.log_file = open(self.value, "w")
+ return ""
+
+
+dap_log = LoggingParam()
+
+
+def log(something):
+ """Log SOMETHING to the log file, if logging is enabled."""
+ if dap_log.log_file is not None:
+ print(something, file=dap_log.log_file)
+ dap_log.log_file.flush()
+
+
+def log_stack():
+ """Log a stack trace to the log file, if logging is enabled."""
+ if dap_log.log_file is not None:
+ traceback.print_exc(file=dap_log.log_file)
+
+
+@in_gdb_thread
+def exec_and_log(cmd):
+ """Execute the gdb command CMD.
+ If logging is enabled, log the command and its output."""
+ log("+++ " + cmd)
+ try:
+ output = gdb.execute(cmd, from_tty=True, to_string=True)
+ if output != "":
+ log(">>> " + output)
+ except gdb.error:
+ log_stack()
+
+
+class Invoker(object):
+ """A simple class that can invoke a gdb command."""
+
+ def __init__(self, cmd):
+ self.cmd = cmd
+
+ # This is invoked in the gdb thread to run the command.
+ @in_gdb_thread
+ def __call__(self):
+ exec_and_log(self.cmd)
+
+
+def send_gdb(cmd):
+ """Send CMD to the gdb thread.
+ CMD can be either a function or a string.
+ If it is a string, it is passed to gdb.execute."""
+ if isinstance(cmd, str):
+ cmd = Invoker(cmd)
+ gdb.post_event(cmd)
+
+
+def send_gdb_with_response(fn):
+ """Send FN to the gdb thread and return its result.
+ If FN is a string, it is passed to gdb.execute and None is
+ returned as the result.
+ If FN throws an exception, this function will throw the
+ same exception in the calling thread.
+ """
+ if isinstance(fn, str):
+ fn = Invoker(fn)
+ result_q = queue.SimpleQueue()
+
+ def message():
+ try:
+ val = fn()
+ result_q.put(val)
+ except Exception as e:
+ result_q.put(e)
+
+ send_gdb(message)
+ val = result_q.get()
+ if isinstance(val, Exception):
+ raise val
+ return val
diff --git a/gdb/python/lib/gdb/dap/state.py b/gdb/python/lib/gdb/dap/state.py
new file mode 100644
index 0000000..fb2e543
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/state.py
@@ -0,0 +1,25 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .startup import in_gdb_thread, exec_and_log, log
+
+
+@in_gdb_thread
+def set_thread(thread_id):
+ """Set the current thread to THREAD_ID."""
+ if thread_id == 0:
+ log("+++ Thread == 0 +++")
+ else:
+ exec_and_log(f"thread {thread_id}")
diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py
new file mode 100644
index 0000000..b6a0ca0
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/threads.py
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# A helper function to construct the list of threads.
+@in_gdb_thread
+def _get_threads():
+ result = []
+ for thr in gdb.selected_inferior().threads():
+ one_result = {
+ "id": thr.global_num,
+ }
+ name = thr.name
+ if name is not None:
+ one_result["name"] = name
+ result.append(one_result)
+ return result
+
+
+@request("threads")
+def threads(**args):
+ result = send_gdb_with_response(_get_threads)
+ return {
+ "threads": result,
+ }
diff --git a/gdb/python/py-dap.c b/gdb/python/py-dap.c
new file mode 100644
index 0000000..8e977bc
--- /dev/null
+++ b/gdb/python/py-dap.c
@@ -0,0 +1,99 @@
+/* Python DAP interpreter
+
+ Copyright (C) 2022 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "defs.h"
+#include "python-internal.h"
+#include "interps.h"
+#include "cli-out.h"
+#include "top.h"
+
+class dap_interp final : public interp
+{
+public:
+
+ explicit dap_interp (const char *name)
+ : interp (name),
+ m_ui_out (new cli_ui_out (gdb_stdout))
+ {
+ }
+
+ ~dap_interp () override = default;
+
+ void init (bool top_level) override;
+
+ void suspend () override
+ {
+ }
+
+ void resume () override
+ {
+ }
+
+ gdb_exception exec (const char *command) override
+ {
+ /* Just ignore it. */
+ return {};
+ }
+
+ void set_logging (ui_file_up logfile, bool logging_redirect,
+ bool debug_redirect) override
+ {
+ /* Just ignore it. */
+ }
+
+ ui_out *interp_ui_out () override
+ {
+ return m_ui_out.get ();
+ }
+
+private:
+
+ std::unique_ptr<ui_out> m_ui_out;
+};
+
+void
+dap_interp::init (bool top_level)
+{
+ gdbpy_enter enter_py;
+
+ gdbpy_ref<> dap_module (PyImport_ImportModule ("gdb.dap"));
+ if (dap_module == nullptr)
+ gdbpy_handle_exception ();
+
+ gdbpy_ref<> func (PyObject_GetAttrString (dap_module.get (), "run"));
+ if (func == nullptr)
+ gdbpy_handle_exception ();
+
+ gdbpy_ref<> result_obj (PyObject_CallNoArgs (func.get ()));
+ if (result_obj == nullptr)
+ gdbpy_handle_exception ();
+
+ current_ui->input_fd = -1;
+ current_ui->m_input_interactive_p = false;
+}
+
+void _initialize_py_interp ();
+void
+_initialize_py_interp ()
+{
+ interp_factory_register ("dap", [] (const char *name) -> interp *
+ {
+ return new dap_interp (name);
+ });
+}
diff --git a/gdb/testsuite/gdb.dap/basic-dap.c b/gdb/testsuite/gdb.dap/basic-dap.c
new file mode 100644
index 0000000..eab1c99
--- /dev/null
+++ b/gdb/testsuite/gdb.dap/basic-dap.c
@@ -0,0 +1,44 @@
+/* Copyright 2022 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+int global_variable = 23;
+
+void
+function_breakpoint_here ()
+{
+ ++global_variable;
+ ++global_variable;
+}
+
+void
+do_not_stop_here ()
+{
+ /* This exists to test that breakpoints are cleared. */
+}
+
+void
+address_breakpoint_here ()
+{
+}
+
+int main ()
+{
+ do_not_stop_here ();
+ function_breakpoint_here ();
+ address_breakpoint_here ();
+ return 0; /* BREAK */
+}
diff --git a/gdb/testsuite/gdb.dap/basic-dap.exp b/gdb/testsuite/gdb.dap/basic-dap.exp
new file mode 100644
index 0000000..d3acf0c
--- /dev/null
+++ b/gdb/testsuite/gdb.dap/basic-dap.exp
@@ -0,0 +1,151 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Basic DAP test.
+
+load_lib dap-support.exp
+
+standard_testfile
+
+if {[build_executable ${testfile}.exp $testfile] == -1} {
+ return
+}
+
+if {[dap_launch $testfile] == ""} {
+ return
+}
+
+set obj [dap_check_request_and_response "set breakpoint on two functions" \
+ setFunctionBreakpoints \
+ {o breakpoints [a [o name [s function_breakpoint_here]] \
+ [o name [s do_not_stop_here]]]}]
+set fn_bpno [dap_get_breakpoint_number $obj]
+
+# This also tests that the previous do_not_stop_here breakpoint is
+# cleared.
+set obj [dap_check_request_and_response "set breakpoint on function" \
+ setFunctionBreakpoints \
+ {o breakpoints [a [o name [s function_breakpoint_here]]]}]
+set fn_bpno [dap_get_breakpoint_number $obj]
+
+set obj [dap_check_request_and_response "set breakpoint with invalid filename" \
+ setBreakpoints \
+ [format {o source [o path [s nosuchfilename.c]] breakpoints [a [o line [i 29]]]}]]
+
+set line [gdb_get_line_number "BREAK"]
+set obj [dap_check_request_and_response "set breakpoint by line number" \
+ setBreakpoints \
+ [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \
+ [list s $srcfile] $line]]
+set line_bpno [dap_get_breakpoint_number $obj]
+
+# Check the new breakpoint event.
+set ok 0
+foreach event [lindex $obj 1] {
+ set d [namespace eval ton::2dict $event]
+ if {[dict get $d type] != "event"
+ || [dict get $d event] != "breakpoint"} {
+ continue
+ }
+ if {[dict get $d body reason] == "new"
+ && [dict get $d body breakpoint verified] == "true"} {
+ set ok 1
+ pass "check new breakpoint event"
+ break
+ }
+}
+if {!$ok} {
+ fail "check new breakpoint event"
+}
+
+set obj [dap_check_request_and_response "reset breakpoint by line number" \
+ setBreakpoints \
+ [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \
+ [list s $srcfile] $line]]
+set new_line_bpno [dap_get_breakpoint_number $obj]
+
+if {$new_line_bpno == $line_bpno} {
+ pass "re-setting kept same breakpoint number"
+} else {
+ fail "re-setting kept same breakpoint number"
+}
+
+# This uses "&address_breakpoint_here" as the address -- this is a
+# hack because we know how this is implemented under the hood.
+set obj [dap_check_request_and_response "set breakpoint by address" \
+ setInstructionBreakpoints \
+ {o breakpoints [a [o instructionReference [s &address_breakpoint_here]]]}]
+set insn_bpno [dap_get_breakpoint_number $obj]
+
+set d [namespace eval ton::2dict [lindex $obj 0]]
+set bplist [dict get $d body breakpoints]
+set insn_pc [dict get [lindex $bplist 0] instructionReference]
+
+dap_check_request_and_response "start inferior" configurationDone
+dap_read_event "inferior started" thread "body reason" started
+
+dap_read_event "stopped at function breakpoint" stopped \
+ "body reason" breakpoint \
+ "body hitBreakpointIds" $fn_bpno
+
+set obj [dap_check_request_and_response "evaluate global in function" \
+ evaluate {o expression [s global_variable]}]
+dap_match_values "global value in function" [lindex $obj 0] \
+ "body result" 23
+
+dap_check_request_and_response step stepIn {o threadId [i 1]}
+dap_read_event "stopped after step" stopped "body reason" step
+
+set obj [dap_check_request_and_response "evaluate global second time" \
+ evaluate {o expression [s global_variable]}]
+dap_match_values "global value after step" [lindex $obj 0] \
+ "body result" 24
+
+dap_check_request_and_response "continue to address" continue
+dap_read_event "stopped at address breakpoint" stopped \
+ "body reason" breakpoint \
+ "body hitBreakpointIds" $insn_bpno
+
+dap_check_request_and_response "continue to line" continue
+dap_read_event "stopped at line breakpoint" stopped \
+ "body reason" breakpoint \
+ "body hitBreakpointIds" $line_bpno
+
+set obj [dap_check_request_and_response "evaluate global in main" \
+ evaluate {o expression [s global_variable]}]
+dap_match_values "global value in main" [lindex $obj 0] \
+ "body result" 25
+
+set obj [dap_request_and_response "evaluate non-existing variable" \
+ evaluate {o expression [s nosuchvariable]}]
+set d [namespace eval ton::2dict [lindex $obj 0]]
+if {[dict get $d success] == "false"} {
+ pass "result of invalid request"
+} else {
+ fail "result of invalid request"
+}
+
+set obj [dap_check_request_and_response "disassemble one instruction" \
+ disassemble \
+ [format {o memoryReference [s %s] instructionCount [i 1]} \
+ $insn_pc]]
+set d [namespace eval ton::2dict [lindex $obj 0]]
+if {[dict exists $d body instructions]} {
+ pass "instructions in disassemble output"
+} else {
+ fail "instructions in disassemble output"
+}
+
+dap_shutdown
diff --git a/gdb/testsuite/lib/dap-support.exp b/gdb/testsuite/lib/dap-support.exp
new file mode 100644
index 0000000..adf332c
--- /dev/null
+++ b/gdb/testsuite/lib/dap-support.exp
@@ -0,0 +1,343 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The JSON parser.
+load_lib ton.tcl
+
+# The sequence number for the next DAP request. This is used by the
+# automatic sequence-counting code below. It is reset each time GDB
+# is restarted.
+set dap_seq 1
+
+# Start gdb using the DAP interpreter.
+proc dap_gdb_start {} {
+ # Keep track of the number of times GDB has been launched.
+ global gdb_instances
+ incr gdb_instances
+
+ gdb_stdin_log_init
+
+ global GDBFLAGS stty_init
+ save_vars { GDBFLAGS stty_init } {
+ set stty_init "-echo raw"
+ set logfile [standard_output_file "dap.log.$gdb_instances"]
+ append GDBFLAGS " -iex \"set debug dap-log-file $logfile\" -q -i=dap"
+ set res [gdb_spawn]
+ if {$res != 0} {
+ return $res
+ }
+ }
+
+ # Reset the counter.
+ set ::dap_seq 1
+
+ return 0
+}
+
+# A helper for dap_to_ton that decides if the list L is a JSON object
+# or if it is an array.
+proc _dap_is_obj {l} {
+ if {[llength $l] % 2 != 0} {
+ return 0
+ }
+ foreach {key value} $l {
+ if {![string is alpha $key]} {
+ return 0
+ }
+ }
+ return 1
+}
+
+# The "TON" format is a bit of a pain to write by hand, so this proc
+# can be used to convert an ordinary Tcl list into TON by guessing at
+# the correct forms to use. This can't be used in all cases, because
+# Tcl can't really differentiate between literal forms. For example,
+# there's no way to decide if "true" should be a string or the literal
+# true.
+#
+# JSON objects must be passed in a particular form here -- as a list
+# with an even number of elements, alternating keys and values. Each
+# key must consist only of letters, no digits or other non-letter
+# characters. Note that this is compatible with the Tcl 'dict'
+# representation.
+proc dap_to_ton {obj} {
+ if {[string is list $obj] && [llength $obj] > 1} {
+ if {[_dap_is_obj $obj]} {
+ set result o
+ foreach {key value} $obj {
+ lappend result $key \[[dap_to_ton $value]\]
+ }
+ } else {
+ set result a
+ foreach val $obj {
+ lappend result \[[dap_to_ton $val]\]
+ }
+ }
+ } elseif {[string is entier $obj]} {
+ set result [list i $obj]
+ } elseif {[string is double $obj]} {
+ set result [list d $obj]
+ } elseif {$obj == "true" || $obj == "false" || $obj == "null"} {
+ set result [list l $obj]
+ } else {
+ set result [list s $obj]
+ }
+ return $result
+}
+
+# Format the object OBJ, in TON format, as JSON and send it to gdb.
+proc dap_send_ton {obj} {
+ set json [namespace eval ton::2json $obj]
+ # FIXME this is wrong for non-ASCII characters.
+ set len [string length $json]
+ verbose ">>> $json"
+ send_gdb "Content-Length: $len\r\n\r\n$json"
+}
+
+# Send a DAP request to gdb. COMMAND is the request's "command"
+# field, and OBJ is the "arguments" field. If OBJ is empty, it is
+# omitted. The sequence number of the request is automatically added,
+# and this is also the return value. OBJ is assumed to already be in
+# TON form.
+proc dap_send_request {command {obj {}}} {
+ # We can construct this directly as a TON object.
+ set result $::dap_seq
+ incr ::dap_seq
+ set req [format {o seq [i %d] type [s request] command [%s]} \
+ $result [list s $command]]
+ if {$obj != ""} {
+ append req " arguments \[$obj\]"
+ }
+ dap_send_ton $req
+ return $result
+}
+
+# Read a JSON response from gdb. This will return a TON object on
+# success, or throw an exception on error.
+proc dap_read_json {} {
+ set length ""
+ gdb_expect {
+ -re "^Content-Length: (\[0-9\]+)\r\n" {
+ set length $expect_out(1,string)
+ exp_continue
+ }
+ -re "^(\[^\r\n\]+)\r\n" {
+ # Any other header field.
+ exp_continue
+ }
+ -re "^\r\n" {
+ # Done.
+ }
+ timeout {
+ error "timeout reading json header"
+ }
+ eof {
+ error "eof reading json header"
+ }
+ }
+
+ if {$length == ""} {
+ error "didn't find content-length"
+ }
+
+ set json ""
+ while {$length > 0} {
+ # Tcl only allows up to 255 characters in a {} expression in a
+ # regexp, so we may need to read in chunks.
+ set this_len [expr {min ($length, 255)}]
+ gdb_expect {
+ -re "^.{$this_len}" {
+ append json $expect_out(0,string)
+ }
+ timeout {
+ error "timeout reading json body"
+ }
+ eof {
+ error "eof reading json body"
+ }
+ }
+ incr length -$this_len
+ }
+
+ return [ton::json2ton $json]
+}
+
+# Read a sequence of JSON objects from gdb, until a response object is
+# seen. If the response object has the request sequence number NUM,
+# and is for command CMD, return a list of two elements: the response
+# object and a list of any preceding events, in the order they were
+# emitted. The objects are in TON format. If a response object is
+# seen but has the wrong sequence number or command, throw an
+# exception
+proc dap_read_response {cmd num} {
+ set result {}
+ while 1 {
+ set obj [dap_read_json]
+ set d [namespace eval ton::2dict $obj]
+ if {[dict get $d type] == "response"} {
+ if {[dict get $d request_seq] != $num} {
+ error "saw wrong request_seq in $obj"
+ } elseif {[dict get $d command] != $cmd} {
+ error "saw wrong command in $obj"
+ } else {
+ return [list $obj $result]
+ }
+ } else {
+ lappend result $obj
+ }
+ }
+}
+
+# A wrapper for dap_send_request and dap_read_response. This sends a
+# request to gdb and returns the result. NAME is used to issue a pass
+# or fail; on failure, this always returns an empty string.
+proc dap_request_and_response {name command {obj {}}} {
+ set result {}
+ if {[catch {
+ set seq [dap_send_request $command $obj]
+ set result [dap_read_response $command $seq]
+ } text]} {
+ verbose "reason: $text"
+ fail $name
+ } else {
+ pass $name
+ }
+ return $result
+}
+
+# Like dap_request_and_response, but also checks that the response
+# indicates success.
+proc dap_check_request_and_response {name command {obj {}}} {
+ set result [dap_request_and_response $name $command $obj]
+ if {$result == ""} {
+ return ""
+ }
+ set d [namespace eval ton::2dict [lindex $result 0]]
+ if {[dict get $d success] != "true"} {
+ verbose "request failure: $result"
+ fail "$name success"
+ return ""
+ }
+ pass "$name success"
+ return $result
+}
+
+# Start gdb, send a DAP initialization request and return the
+# response. This approach lets the caller check the feature list, if
+# desired. Callers not caring about this should probably use
+# dap_launch. Returns the empty string on failure. NAME is used as
+# the test name.
+proc dap_initialize {name} {
+ if {[dap_gdb_start]} {
+ return ""
+ }
+ return [dap_check_request_and_response $name initialize]
+}
+
+# Start gdb, send a DAP initialize request, and then a launch request
+# specifying FILE as the program to use for the inferior. Returns the
+# empty string on failure, or the response object from the launch
+# request. After this is called, gdb will be ready to accept
+# breakpoint requests. NAME is used as the test name. It has a
+# reasonable default but can be overridden in case a test needs to
+# launch gdb more than once.
+proc dap_launch {file {name startup}} {
+ if {[dap_initialize "$name - initialize"] == ""} {
+ return ""
+ }
+ return [dap_check_request_and_response "$name - launch" launch \
+ [format {o program [%s]} \
+ [list s [standard_output_file $file]]]]
+}
+
+# Cleanly shut down gdb. NAME is used as the test name.
+proc dap_shutdown {{name shutdown}} {
+ dap_check_request_and_response $name disconnect
+}
+
+# Search the event list EVENTS for an output event matching the regexp
+# RX. Pass the test NAME if found, fail if not.
+proc dap_search_output {name rx events} {
+ foreach event $events {
+ set d [namespace eval ton::2dict $event]
+ if {[dict get $d type] != "event"
+ || [dict get $d event] != "output"} {
+ continue
+ }
+ if {[regexp $rx [dict get $d body output]]} {
+ pass $name
+ return
+ }
+ }
+ fail $name
+}
+
+# Check that OBJ (a single TON object) has values that match the
+# key/value pairs given in ARGS. NAME is used as the test name.
+proc dap_match_values {name obj args} {
+ set d [namespace eval ton::2dict $obj]
+ foreach {key value} $args {
+ if {[eval dict get [list $d] $key] != $value} {
+ fail "$name (checking $key)"
+ return ""
+ }
+ }
+ pass $name
+}
+
+# A helper for dap_read_event that reads events, looking for one
+# matching TYPE.
+proc _dap_read_event {type} {
+ while 1 {
+ # We don't do any extra error checking here for the time
+ # being; we'll just get a timeout thrown instead.
+ set obj [dap_read_json]
+ set d [namespace eval ton::2dict $obj]
+ if {[dict get $d type] == "event"
+ && [dict get $d event] == $type} {
+ return $obj
+ }
+ }
+}
+
+# Read JSON objects looking for an event whose "event" field is TYPE.
+# NAME is used as the test name; it defaults to TYPE. Extra arguments
+# are used to check fields of the event; the arguments alternate
+# between a field name (in "dict get" form) and its expected value.
+# Returns the TON object for the chosen event, or empty string on
+# error.
+proc dap_read_event {name type args} {
+ if {$name == ""} {
+ set name $type
+ }
+
+ if {[catch {_dap_read_event $type} result]} {
+ fail $name
+ return ""
+ }
+
+ eval dap_match_values [list $name $result] $args
+
+ return $result
+}
+
+# A convenience function to extract the breakpoint number when a new
+# breakpoint is created. OBJ is an object as returned by
+# dap_check_request_and_response.
+proc dap_get_breakpoint_number {obj} {
+ set d [namespace eval ton::2dict [lindex $obj 0]]
+ set bplist [dict get $d body breakpoints]
+ return [dict get [lindex $bplist 0] id]
+}
diff --git a/gdb/testsuite/lib/mi-support.exp b/gdb/testsuite/lib/mi-support.exp
index e9226ad..1ee087d 100644
--- a/gdb/testsuite/lib/mi-support.exp
+++ b/gdb/testsuite/lib/mi-support.exp
@@ -238,8 +238,6 @@ proc default_mi_gdb_start { { flags {} } } {
return [mi_gdb_start_separate_mi_tty $flags]
}
- set inferior_pty no-tty
-
# Set the default value, it may be overriden later by specific testfile.
set use_gdb_stub [target_info exists use_gdb_stub]
diff --git a/gdb/testsuite/lib/ton.tcl b/gdb/testsuite/lib/ton.tcl
new file mode 100644
index 0000000..a9013b9
--- /dev/null
+++ b/gdb/testsuite/lib/ton.tcl
@@ -0,0 +1,303 @@
+# This was imported into gdb from:
+# https://github.com/jorge-leon/ton
+
+# This software is copyrighted by Georg Lehner <jorge@at.anteris.net>.
+# The following terms apply to all files associated with the software
+# unless explicitly disclaimed in individual files.
+
+# The authors hereby grant permission to use, copy, modify, distribute,
+# and license this software and its documentation for any purpose,
+# provided that existing copyright notices are retained in all copies
+# and that this notice is included verbatim in any distributions. No
+# written agreement, license, or royalty fee is required for any of the
+# authorized uses. Modifications to this software may be copyrighted by
+# their authors and need not follow the licensing terms described here,
+# provided that the new terms are clearly indicated on the first page of
+# each file where they apply.
+
+# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
+# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
+# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
+# NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND
+# THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE
+# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+# GOVERNMENT USE: If you are acquiring this software on behalf of the
+# U.S. government, the Government shall have only "Restricted Rights" in
+# the software and related documentation as defined in the Federal
+# Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
+# are acquiring the software on behalf of the Department of Defense, the
+# software shall be classified as "Commercial Computer Software" and the
+# Government shall have only "Restricted Rights" as defined in Clause
+# 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
+# authors grant the U.S. Government and others acting in its behalf
+# permission to use and distribute the software in accordance with the
+# terms specified in this license.
+
+
+# leg20180331: ton / TON - Tcl Object Notation
+#
+# This package provides manipulation functionality for TON - a data
+# serialization format with a direct mapping to JSON.
+#
+# In its essence, a JSON parser is provided, which can convert a JSON
+# string into a Tcllib json style dictionary (dicts and arrays mixed),
+# into a jimhttp style dictionary (only dicts) or into a nested, typed
+# Tcl list.
+#
+# Finally, TON can be converted into (unformatted) JSON.
+
+namespace eval ton {
+ namespace export json2ton
+
+ variable version 0.4
+
+}
+proc ton::json2ton json {
+ # Parse JSON string json
+ #
+ # return: TON
+
+ set i [trr $json [string length $json]]
+ if {!$i} {return ""}
+ lassign [jscan $json $i] i ton
+ if {[set i [trr $json $i]]} {
+ error "json string invalid:[incr i -1]: left over characters."
+ }
+ return $ton
+}
+proc ton::trr {s i} {
+ # Trim righthand whitespace on the first i characters of s.
+ # return: number of remaining characters in s
+
+ while {[set j $i] &&
+ ([string is space [set c [string index $s [incr i -1]]]]
+ || $c eq "\n")} {}
+ return $j
+}
+proc ton::jscan {json i {d :}} {
+ # Scan JSON in first i characters of string json.
+ # d is the default delimiter list for the next token.
+ #
+ # return list of
+ # - remaining characters in json
+ # - TON
+ #
+ # The string must already be whitespace trimmed from the right.
+
+ incr i -1
+
+ if {[set c [string index $json $i]] eq "\""} {
+ str $json [incr i -1]
+ } elseif {$c eq "\}"} {
+ obj $json $i
+ } elseif {$c eq "\]"} {
+ arr $json $i
+ } elseif {$c in {e l}} {
+ lit $json $i
+ } elseif {[string match {[0-9.]} $c]} {
+ num $json $i $c $d
+ } else {
+ error "json string end invalid:$i: ..[string range $json $i-10 $i].."
+ }
+}
+proc ton::num {json i c d} {
+ # Parse number from position i in string json to the left.
+ # c .. character at position i
+ # d .. delimiter on which to stop
+ #
+ # return list:
+ # - remaining string length
+ # - TON of number
+
+ set float [expr {$c eq "."}]
+ for {set j $i} {$i} {incr i -1} {
+ if {[string match $d [set c [string index $json $i-1]]]} break
+ set float [expr {$float || [string match "\[eE.]" $c]}]
+ }
+ set num [string trimleft [string range $json $i $j]]
+ if {!$float && [string is entier $num]} {
+ list $i "i $num"
+ } elseif {$float && [string is double $num]} {
+ list $i "d $num"
+ } else {
+ error "number invalid:$i: $num."
+ }
+}
+proc ton::lit {json i} {
+ # Parse literal from position i in string json to the left
+ # return list:
+ # - remaining string length
+ # - TON of literal
+
+ if {[set c [string index $json $i-1]] eq "u"} {
+ list [incr i -3] "l true"
+ } elseif {$c eq "s"} {
+ list [incr i -4] "l false"
+ } elseif {$c eq "l"} {
+ list [incr i -3] "l null"
+ } else {
+ set e [string range $json $i-3 $i]
+ error "literal invalid:[incr i -1]: ..$e."
+ }
+}
+proc ton::str {json i} {
+ # Parse string from position i in string json to the left
+ # return list:
+ # - remaining string length
+ # - TON of string
+
+ for {set j $i} {$i} {incr i -1} {
+ set i [string last \" $json $i]
+ if {[string index $json $i-1] ne "\\"} break
+ }
+ if {$i==-1} {
+ error "json string start invalid:$i: exhausted while parsing string."
+ }
+ list $i "s [list [string range $json $i+1 $j]]"
+}
+proc ton::arr {json i} {
+ # Parse array from i characters in string json
+ # return list:
+ # - remaining string length
+ # - TON of array
+
+ set i [trr $json $i]
+ if {!$i} {
+ error "json string invalid:0: exhausted while parsing array."
+ }
+ if {[string index $json $i-1] eq "\["} {
+ return [list [incr i -1] a]
+ }
+ set r {}
+ while {$i} {
+ lassign [jscan $json $i "\[,\[]"] i v
+ lappend r \[$v\]
+ set i [trr $json $i]
+ incr i -1
+ if {[set c [string index $json $i]] eq ","} {
+ set i [trr $json $i]
+ continue
+ } elseif {$c eq "\["} break
+ error "json string invalid:$i: parsing array."
+ }
+ lappend r a
+ return [list $i [join [lreverse $r]]]
+}
+proc ton::obj {json i} {
+ # Parse array from i character in string json
+ # return list:
+ # - remaining string length
+ # - TON of object
+
+ set i [trr $json $i]
+ if {!$i} {
+ error "json string invalid:0: exhausted while parsing object."
+ }
+ if {[string index $json $i-1] eq "\{"} {
+ return [list [incr i -1] o]
+ }
+ set r {}
+ while {$i} {
+ lassign [jscan $json $i] i v
+ set i [trr $json $i]
+ incr i -1
+ if {[string index $json $i] ne ":"} {
+ error "json string invalid:$i: parsing key in object."
+ }
+ set i [trr $json $i]
+ lassign [jscan $json $i] i k
+ lassign $k type k
+ if {$type ne "s"} {
+ error "json string invalid:[incr i -1]: key not a string."
+ }
+ lappend r \[$v\] [list $k]
+ set i [trr $json $i]
+ incr i -1
+ if {[set c [string index $json $i]] eq ","} {
+ set i [trr $json $i]
+ continue
+ } elseif {$c eq "\{"} break
+ error "json string invalid:$i: parsing object."
+ }
+ lappend r o
+ return [list $i [join [lreverse $r]]]
+}
+# TON decoders
+namespace eval ton::2list {
+ proc atom {type v} {list $type $v}
+ foreach type {i d s l} {
+ interp alias {} $type {} [namespace current]::atom $type
+ }
+ proc a args {
+ set r a
+ foreach v $args {lappend r $v}
+ return $r
+ }
+ proc o args {
+ set r o
+ foreach {k v} $args {lappend r $k $v}
+ return $r
+ }
+ # There is plenty of room for validation in get
+ # array index bounds
+ # object key existence
+ proc get {l args} {
+ foreach k $args {
+ switch [lindex $l 0] {
+ o {set l [dict get [lrange $l 1 end] $k]}
+ a {set l [lindex $l [incr k]]}
+ default {
+ error "error: key $k to long, or wrong data: [lindex $l 0]"
+ }
+ }
+ }
+ return $l
+ }
+}
+namespace eval ton::2dict {
+ proc atom v {return $v}
+ foreach type {i d l s} {
+ interp alias {} $type {} [namespace current]::atom
+ }
+ proc a args {return $args}
+ proc o args {return $args}
+}
+namespace eval ton::a2dict {
+ proc atom v {return $v}
+ foreach type {i d l s} {
+ interp alias {} $type {} [namespace current]::atom
+ }
+ proc a args {
+ set i -1
+ set r {}
+ foreach v $args {
+ lappend r [incr i] $v
+ }
+ return $r
+ }
+ proc o args {return $args}
+}
+namespace eval ton::2json {
+ proc atom v {return $v}
+ foreach type {i d l} {
+ interp alias {} $type {} [namespace current]::atom
+ }
+ proc a args {
+ return "\[[join $args {, }]]"
+ }
+ proc o args {
+ set r {}
+ foreach {k v} $args {lappend r "\"$k\": $v"}
+ return "{[join $r {, }]}"
+ }
+ proc s s {return "\"$s\""}
+}
+
+package provide ton $ton::version