From a9092db37556ab059b1302bebf3be120b594dc59 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 1 Aug 2026 20:06:34 +0800 Subject: [PATCH] debug: add LLDB goroutine backtraces --- cmd/internal/lldb/lldb_test.go | 2 + cmd/internal/lldb/llgo_plugin.py | 104 ++++++++++++++++++-- cmd/llgo/lldbtest/README.md | 9 +- cmd/llgo/lldbtest/runtest.sh | 4 +- cmd/llgo/lldbtest/test.py | 58 +++++++++-- runtime/internal/runtime/g_global.go | 3 +- runtime/internal/runtime/g_tls.go | 2 + runtime/internal/runtime/proc.go | 15 +++ runtime/internal/runtime/runtime2.go | 9 +- runtime/internal/runtime/threadid_darwin.go | 37 +++++++ runtime/internal/runtime/threadid_linux.go | 37 +++++++ runtime/internal/runtime/threadid_other.go | 23 +++++ test/llgoext/runtime_g_test.go | 21 ++++ 13 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 runtime/internal/runtime/threadid_darwin.go create mode 100644 runtime/internal/runtime/threadid_linux.go create mode 100644 runtime/internal/runtime/threadid_other.go diff --git a/cmd/internal/lldb/lldb_test.go b/cmd/internal/lldb/lldb_test.go index cbeedeb7a4..d4e365bfc1 100644 --- a/cmd/internal/lldb/lldb_test.go +++ b/cmd/internal/lldb/lldb_test.go @@ -194,10 +194,12 @@ func TestEmbeddedPluginIdentity(t *testing.T) { "ChannelSyntheticProvider", "LLGO_GOROUTINE_LAYOUTS", "print_goroutines", + "print_goroutine", "llgo status", "llgo print", "llgo vars", "llgo goroutines", + "llgo goroutine", } { if !strings.Contains(source, want) { t.Errorf("embedded plugin is missing %q", want) diff --git a/cmd/internal/lldb/llgo_plugin.py b/cmd/internal/lldb/llgo_plugin.py index c05cc3df0c..de791d50c5 100644 --- a/cmd/internal/lldb/llgo_plugin.py +++ b/cmd/internal/lldb/llgo_plugin.py @@ -16,6 +16,7 @@ LLGO_DEFAULT_MAX_CHILDREN = 256 LLGO_MAX_CONTAINER_SCAN_BUCKETS = 65536 LLGO_MAX_GOROUTINES = 65536 +LLGO_MAX_STACK_FRAMES = 256 _TARGET_INFO_CACHE: Dict[Tuple[Any, ...], "LLGoTargetInfo"] = {} @@ -154,6 +155,7 @@ class LLGoGoroutineLayout: m_current_goroutine: str m_p: str m_id: str + m_procid: str p_m: str p_id: str status_names: Tuple[Tuple[int, str], ...] @@ -168,6 +170,7 @@ class LLGoGoroutineValue: status_name: str mid: int pid: int + procid: int ownership_linked: bool @@ -271,6 +274,7 @@ def _goroutine_layouts() -> Dict[int, LLGoGoroutineLayout]: m_current_goroutine=goroutine["m_current_goroutine"], m_p=goroutine["m_p"], m_id=goroutine["m_id"], + m_procid=goroutine["m_procid"], p_m=goroutine["p_m"], p_id=goroutine["p_id"], status_names=status_names, @@ -332,6 +336,8 @@ def register_commands(debugger: lldb.SBDebugger) -> None: 'command script add -f llgo_plugin.print_all_variables llgo vars') debugger.HandleCommand( 'command script add -f llgo_plugin.print_goroutines llgo goroutines') + debugger.HandleCommand( + 'command script add -f llgo_plugin.print_goroutine llgo goroutine') if inspect_target(debugger.GetSelectedTarget()).supported: register_type_formatters(debugger) @@ -773,6 +779,9 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess, if not m_value or not m_value.IsValid(): raise ValueError(f"cannot read M for goroutine {goid}") mid = _required_integer_field(m_value, layout.m_id) + procid_value = _value_as_int( + m_value.GetChildMemberWithName(layout.m_procid)) + procid = procid_value if procid_value is not None else 0 current_g = _required_integer_field( m_value, layout.m_current_goroutine) p_address = _required_integer_field(m_value, layout.m_p) @@ -792,6 +801,7 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess, status_name=status_names.get(status, f"status-{status}"), mid=mid, pid=pid, + procid=procid, ownership_linked=(current_g == address and p_m == m_address), )) address = next_address @@ -800,6 +810,18 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess, return values +def _goroutine_thread(process: lldb.SBProcess, + goroutine: LLGoGoroutineValue) -> Optional[lldb.SBThread]: + if goroutine.procid == 0: + return None + for index in range(process.GetNumThreads()): + thread = process.GetThreadAtIndex(index) + if (thread and thread.IsValid() and + thread.GetThreadID() == goroutine.procid): + return thread + return None + + def print_goroutines(debugger: lldb.SBDebugger, command: str, result: lldb.SBCommandReturnObject, _internal_dict: Dict[str, Any]) -> None: @@ -827,13 +849,81 @@ def print_goroutines(debugger: lldb.SBDebugger, command: str, if not goroutines: result.AppendMessage("No live LLGo goroutines.") return - result.AppendMessage("\n".join( - f"goroutine {goroutine.goid} [{goroutine.status_name}] " - f"parent={goroutine.parent_goid} m={goroutine.mid} " - f"p={goroutine.pid} ownership=" - f"{'linked' if goroutine.ownership_linked else 'invalid'}" - for goroutine in goroutines - )) + lines = [] + for goroutine in goroutines: + thread = _goroutine_thread(process, goroutine) + thread_index = (str(thread.GetIndexID()) + if thread is not None else "unavailable") + lines.append( + f"goroutine {goroutine.goid} [{goroutine.status_name}] " + f"parent={goroutine.parent_goid} m={goroutine.mid} " + f"p={goroutine.pid} thread={thread_index} ownership=" + f"{'linked' if goroutine.ownership_linked else 'invalid'}") + result.AppendMessage("\n".join(lines)) + + +def _frame_description(frame: lldb.SBFrame, index: int) -> str: + name = frame.GetFunctionName() or frame.GetSymbol().GetName() or "" + description = f"frame #{index}: {name}" + line_entry = frame.GetLineEntry() + if line_entry and line_entry.IsValid(): + file_spec = line_entry.GetFileSpec() + filename = file_spec.GetFilename() if file_spec else None + line = line_entry.GetLine() + if filename and line: + description += f" at {filename}:{line}" + return description + + +def print_goroutine(debugger: lldb.SBDebugger, command: str, + result: lldb.SBCommandReturnObject, + _internal_dict: Dict[str, Any]) -> None: + if not _require_supported_target(debugger, result): + return + match = re.fullmatch(r"\s*([0-9]+)\s+(?:bt|backtrace)\s*", command) + if match is None: + result.SetError("usage: llgo goroutine bt") + return + process = _stopped_process(debugger, result) + if process is None: + return + + target = debugger.GetSelectedTarget() + info = inspect_target(target) + layout = LLGO_GOROUTINE_LAYOUTS.get(info.runtime_layout_version) + if layout is None: + result.SetError("LLGo goroutine metadata is unavailable for this runtime.") + return + try: + goroutines = _goroutine_values(target, process, layout) + except ValueError as error: + result.SetError(str(error)) + return + + goid = int(match.group(1)) + goroutine = next( + (candidate for candidate in goroutines if candidate.goid == goid), + None) + if goroutine is None: + result.SetError(f"LLGo goroutine {goid} is not live.") + return + thread = _goroutine_thread(process, goroutine) + if thread is None: + result.SetError( + f"LLGo goroutine {goid} has no matching debugger thread.") + return + + frame_count = min(thread.GetNumFrames(), LLGO_MAX_STACK_FRAMES) + lines = [ + f"goroutine {goid} [{goroutine.status_name}] " + f"thread {thread.GetIndexID()}:" + ] + lines.extend(_frame_description(thread.GetFrameAtIndex(index), index) + for index in range(frame_count)) + if thread.GetNumFrames() > frame_count: + lines.append( + f"... ({thread.GetNumFrames() - frame_count} more frames)") + result.AppendMessage("\n".join(lines)) def _value_as_int(value: lldb.SBValue) -> Optional[int]: diff --git a/cmd/llgo/lldbtest/README.md b/cmd/llgo/lldbtest/README.md index fde046215f..1f3833cd2d 100644 --- a/cmd/llgo/lldbtest/README.md +++ b/cmd/llgo/lldbtest/README.md @@ -40,8 +40,8 @@ llgo lldb -lldb /opt/homebrew/bin/lldb -- --batch ./cl/_testdata/debug/out The command embeds and loads the LLGo Python adapter, so an installed `llgo` does not depend on a source checkout. `cmd/llgo/lldbtest/runlldb.sh` remains as a thin compatibility wrapper. Adapter commands live under `llgo`, including -`llgo status`, `llgo print`, `llgo vars`, and `llgo goroutines`; stock LLDB -commands and aliases +`llgo status`, `llgo print`, `llgo vars`, `llgo goroutines`, and +`llgo goroutine ID bt`; stock LLDB commands and aliases such as `p` and `v` are left unchanged. `llgo status` reports the recognized debugger schema, runtime-layout version, target triple, pointer size, and byte order. Unknown marker versions disable only the LLGo-specific commands; raw @@ -53,8 +53,9 @@ goroutines. Maps expose their length and typed key/value children, including indirect large entries; channels expose length, capacity, closed state, and buffered values in receive order. `llgo goroutines` reports each live goroutine's runtime state, parent, -and G/M/P ownership. Goroutine-aware stack selection is separate follow-up -work. Named container types are covered as well as predeclared types. Explicit +G/M/P ownership, and matching debugger thread. `llgo goroutine ID bt` prints +that goroutine's native stack without changing the selected LLDB thread. Named +container types are covered as well as predeclared types. Explicit `llgo print` slice views respect LLDB's `target.max-children-count` setting. Ordinary C targets and targets with unknown or ambiguous LLGo markers retain LLDB's raw presentation. diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 92287d40b2..403a711b70 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -94,7 +94,8 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1); LLGo ABI v1; C ABI mode 2 (allfunc)" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo print s", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutine 1 bt", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' # The LLGo formatter must not attach itself to an ordinary C target. non_llgo_dir="$test_tmp_dir/non-llgo" @@ -116,6 +117,7 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsuppor -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutine 1 bt", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' # Multiple marker versions are ambiguous even when one version is supported. diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 288815507e..7e5ee54109 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -272,8 +272,10 @@ def test_case(marker: str, expectations: List[tuple]) -> TestCase: test_case("goroutine_values", [ ("goroutineReadySum", "3"), ("goroutines", - "count=3 roots=1 children=2 running=3 linked=3 unique-m=3 unique-p=3", + "count=3 roots=1 children=2 running=3 linked=3 mapped=3 " + "unique-m=3 unique-p=3 unique-threads=3", "goroutines"), + ("goroutine stacks", "root=1 children=2", "goroutine-stacks"), ]), test_case("struct_values_initial", STRUCT_VALUES_INITIAL), test_case("struct_values_updated", STRUCT_VALUES_UPDATED), @@ -482,7 +484,7 @@ def require_print_error(self, expression: str, expected: str) -> None: f"llgo print {expression!r} did not fail with {expected!r}: " f"{result.GetOutput()!r} {result.GetError()!r}") - def get_goroutine_summary(self) -> Optional[str]: + def get_goroutines(self) -> Optional[List[Dict[str, Any]]]: result = lldb.SBCommandReturnObject() self.debugger.GetCommandInterpreter().HandleCommand( "llgo goroutines", result) @@ -492,7 +494,8 @@ def get_goroutine_summary(self) -> Optional[str]: if line.strip()] pattern = re.compile( r"^goroutine ([0-9]+) \[([^]]+)\] parent=([0-9]+) " - r"m=(-?[0-9]+) p=(-?[0-9]+) ownership=(linked|invalid)$") + r"m=(-?[0-9]+) p=(-?[0-9]+) " + r"thread=(unavailable|[0-9]+) ownership=(linked|invalid)$") goroutines = [] for line in lines: match = pattern.fullmatch(line) @@ -504,8 +507,15 @@ def get_goroutine_summary(self) -> Optional[str]: "parent": int(match.group(3)), "mid": int(match.group(4)), "pid": int(match.group(5)), - "ownership": match.group(6), + "thread": match.group(6), + "ownership": match.group(7), }) + return goroutines + + def get_goroutine_summary(self) -> Optional[str]: + goroutines = self.get_goroutines() + if goroutines is None: + return None ids = {goroutine["goid"] for goroutine in goroutines} roots = sum(goroutine["parent"] == 0 for goroutine in goroutines) children = sum( @@ -515,12 +525,46 @@ def get_goroutine_summary(self) -> Optional[str]: goroutine["status"] == "running" for goroutine in goroutines) linked = sum( goroutine["ownership"] == "linked" for goroutine in goroutines) + mapped = sum( + goroutine["thread"] != "unavailable" for goroutine in goroutines) unique_m = len({goroutine["mid"] for goroutine in goroutines}) unique_p = len({goroutine["pid"] for goroutine in goroutines}) + unique_threads = len({goroutine["thread"] for goroutine in goroutines + if goroutine["thread"] != "unavailable"}) return ( f"count={len(goroutines)} roots={roots} children={children} " - f"running={running} linked={linked} unique-m={unique_m} " - f"unique-p={unique_p}") + f"running={running} linked={linked} mapped={mapped} " + f"unique-m={unique_m} unique-p={unique_p} " + f"unique-threads={unique_threads}") + + def get_goroutine_stack_summary(self) -> Optional[str]: + goroutines = self.get_goroutines() + if goroutines is None: + return None + roots = [goroutine for goroutine in goroutines + if goroutine["parent"] == 0] + if len(roots) != 1: + return None + children = [goroutine for goroutine in goroutines + if goroutine["parent"] == roots[0]["goid"]] + if len(children) != 2: + return None + + expected_functions = [(roots[0], "main.InspectGoroutineValues")] + expected_functions.extend( + (goroutine, "main.RuntimeGoroutineValues") + for goroutine in children) + for goroutine, expected_function in expected_functions: + result = lldb.SBCommandReturnObject() + self.debugger.GetCommandInterpreter().HandleCommand( + f"llgo goroutine {goroutine['goid']} bt", result) + output = result.GetOutput() or "" + if (not result.Succeeded() or + expected_function not in output or + f"goroutine {goroutine['goid']} [running] thread " + not in output): + return None + return f"root={len(roots)} children={len(children)}" def cleanup(self) -> None: if self.process and self.process.IsValid(): @@ -711,6 +755,8 @@ def execute_single_variable_test(debugger: LLDBDebugger, test: Test) -> TestResu "settings set target.max-children-count 256") elif test.mode == "goroutines": actual_value = debugger.get_goroutine_summary() + elif test.mode == "goroutine-stacks": + actual_value = debugger.get_goroutine_stack_summary() else: actual_value = debugger.get_variable_value(test.variable) if actual_value is None: diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go index d373278a9c..f9540f375b 100644 --- a/runtime/internal/runtime/g_global.go +++ b/runtime/internal/runtime/g_global.go @@ -23,11 +23,12 @@ var currentG *g func getg() *g { if currentG == nil { - currentG = initRuntimeContext(new(runtimeContext), nil, _Grunning) + setg(initRuntimeContext(new(runtimeContext), nil, _Grunning)) } return currentG } func setg(gp *g) { currentG = gp + setMProcID(gp) } diff --git a/runtime/internal/runtime/g_tls.go b/runtime/internal/runtime/g_tls.go index 30dee1695a..2c984c37b7 100644 --- a/runtime/internal/runtime/g_tls.go +++ b/runtime/internal/runtime/g_tls.go @@ -86,6 +86,7 @@ func setg(gp *g) { } } currentG = uintptr(unsafe.Pointer(gp)) + setMProcID(gp) } func setAutoG(gp *g) c.Int { @@ -94,6 +95,7 @@ func setAutoG(gp *g) c.Int { } currentG = uintptr(unsafe.Pointer(gp)) currentGHasLifecycle = true + setMProcID(gp) return 0 } diff --git a/runtime/internal/runtime/proc.go b/runtime/internal/runtime/proc.go index f17ca4f662..4f12642e3b 100644 --- a/runtime/internal/runtime/proc.go +++ b/runtime/internal/runtime/proc.go @@ -182,6 +182,12 @@ func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { return gp } +func setMProcID(gp *g) { + if gp != nil && gp.m != nil { + gp.m.procid = currentThreadID() + } +} + func registerG(gp *g) { lockAllg() gp.alllink = debuggerAllgV1 @@ -256,6 +262,15 @@ func GInAllGForTesting(target unsafe.Pointer) (found, linked bool) { return found, true } +// ProcIDForTesting reports the OS thread identity used by debuggers. +func ProcIDForTesting() uint64 { + gp := getg() + if gp == nil || gp.m == nil { + return 0 + } + return gp.m.procid +} + // GMPForTesting reports the current runtime ownership graph. It is kept // internal to the compiler runtime and linked only by LLGo execution tests. func GMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) { diff --git a/runtime/internal/runtime/runtime2.go b/runtime/internal/runtime/runtime2.go index 6dabbd65e3..b21ba7c60a 100644 --- a/runtime/internal/runtime/runtime2.go +++ b/runtime/internal/runtime/runtime2.go @@ -63,10 +63,11 @@ type g struct { // thread handle is deliberately confined to mOS so other backends do not leak // pthread types into the scheduler core. type m struct { - curg *g - p *p - id int64 - os mOS + curg *g + p *p + id int64 + procid uint64 // OS thread ID used by debuggers. + os mOS } // p represents the scheduling resources attached to an M. The pthread backend diff --git a/runtime/internal/runtime/threadid_darwin.go b/runtime/internal/runtime/threadid_darwin.go new file mode 100644 index 0000000000..269396f4ab --- /dev/null +++ b/runtime/internal/runtime/threadid_darwin.go @@ -0,0 +1,37 @@ +//go:build llgo && darwin && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + _ "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/pthread" +) + +//go:linkname pthreadThreadID C.pthread_threadid_np +func pthreadThreadID(thread pthread.Thread, threadID *uint64) c.Int + +func currentThreadID() uint64 { + var threadID uint64 + if pthreadThreadID(nil, &threadID) != 0 { + return 0 + } + return threadID +} diff --git a/runtime/internal/runtime/threadid_linux.go b/runtime/internal/runtime/threadid_linux.go new file mode 100644 index 0000000000..d16e299a72 --- /dev/null +++ b/runtime/internal/runtime/threadid_linux.go @@ -0,0 +1,37 @@ +//go:build llgo && linux && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + _ "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/syscall" +) + +//go:linkname cSyscall C.syscall +func cSyscall(number c.Long, args ...any) c.Long + +func currentThreadID() uint64 { + threadID := cSyscall(c.Long(syscall.SYS_GETTID)) + if threadID <= 0 { + return 0 + } + return uint64(threadID) +} diff --git a/runtime/internal/runtime/threadid_other.go b/runtime/internal/runtime/threadid_other.go new file mode 100644 index 0000000000..15f94af8ff --- /dev/null +++ b/runtime/internal/runtime/threadid_other.go @@ -0,0 +1,23 @@ +//go:build !llgo || baremetal || (!darwin && !linux) + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +func currentThreadID() uint64 { + return 0 +} diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go index 65d273c195..04dc3e750d 100644 --- a/test/llgoext/runtime_g_test.go +++ b/test/llgoext/runtime_g_test.go @@ -19,6 +19,7 @@ package llgoext import ( + goruntime "runtime" "testing" "unsafe" ) @@ -79,11 +80,16 @@ func TestRuntimeGetGIsolation(t *testing.T) { //go:linkname runtimeGMPForTesting github.com/goplus/llgo/runtime/internal/runtime.GMPForTesting func runtimeGMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) +//go:linkname runtimeProcIDForTesting github.com/goplus/llgo/runtime/internal/runtime.ProcIDForTesting +func runtimeProcIDForTesting() uint64 + const ( runtimeGRunning = 2 runtimePRunning = 1 ) +var runtimeHasDebuggerThreadID = goruntime.GOOS == "darwin" || goruntime.GOOS == "linux" + type runtimeGMPState struct { goid uint64 parentGoid uint64 @@ -91,6 +97,7 @@ type runtimeGMPState struct { pid int32 gstatus uint32 pstatus uint32 + procid uint64 linked bool } @@ -103,6 +110,7 @@ func currentRuntimeGMPState() runtimeGMPState { pid: pid, gstatus: gstatus, pstatus: pstatus, + procid: runtimeProcIDForTesting(), linked: linked, } } @@ -118,6 +126,9 @@ func checkRunningRuntimeGMP(t *testing.T, state runtimeGMPState) { if state.pid < 0 { t.Fatalf("current G has invalid P id %d", state.pid) } + if runtimeHasDebuggerThreadID && state.procid == 0 { + t.Fatal("current M has no debugger thread ID") + } if state.gstatus != runtimeGRunning { t.Fatalf("G status = %d, want running (%d)", state.gstatus, runtimeGRunning) } @@ -143,6 +154,10 @@ func TestRuntimeGMPLinks(t *testing.T) { seenG := map[uint64]bool{parent.goid: true} seenM := map[int64]bool{parent.mid: true} seenP := map[int32]bool{parent.pid: true} + seenProcID := map[uint64]bool{} + if runtimeHasDebuggerThreadID { + seenProcID[parent.procid] = true + } for i := 0; i < cap(results); i++ { state := <-results checkRunningRuntimeGMP(t, state) @@ -158,9 +173,15 @@ func TestRuntimeGMPLinks(t *testing.T) { if seenP[state.pid] { t.Fatalf("duplicate P id %d", state.pid) } + if runtimeHasDebuggerThreadID && seenProcID[state.procid] { + t.Fatalf("duplicate debugger thread ID %d", state.procid) + } seenG[state.goid] = true seenM[state.mid] = true seenP[state.pid] = true + if runtimeHasDebuggerThreadID { + seenProcID[state.procid] = true + } } }