https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/209923
Starting a protocol server (such as MCP, and in the future potentially DAP) is only possible from the command line today, via `protocol-server start`. Add an SB API so an embedder can start one in its own process and learn where to connect. SBProtocolServer wraps ProtocolServer::GetOrCreate/Start/Stop and exposes the listening connection URI. This lets a tool host the engine's protocol server in-process and talk to it as a normal client, reusing the server's tools rather than reimplementing them. Assisted-by: Claude >From 0ecf63c47a00bed7d6cb1d7b35f47fa5399783bc Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Wed, 8 Jul 2026 17:31:48 -0700 Subject: [PATCH] [lldb] Add SBProtocolServer to start protocol servers programmatically Starting a protocol server (MCP, DAP, ...) is only possible from the command line today, via `protocol-server start`. Add an SB API so an embedder can start one in its own process and learn where to connect. SBProtocolServer wraps ProtocolServer::GetOrCreate/Start/Stop and exposes the listening connection URI. This lets a tool host the engine's protocol server in-process and talk to it as a normal client, reusing the server's tools rather than reimplementing them. Assisted-by: Claude --- lldb/bindings/interfaces.swig | 1 + lldb/include/lldb/API/LLDB.h | 1 + lldb/include/lldb/API/SBDefines.h | 3 +- lldb/include/lldb/API/SBProtocolServer.h | 54 +++++++ lldb/source/API/CMakeLists.txt | 1 + lldb/source/API/SBProtocolServer.cpp | 160 ++++++++++++++++++++ lldb/unittests/API/CMakeLists.txt | 1 + lldb/unittests/API/SBProtocolServerTest.cpp | 128 ++++++++++++++++ 8 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 lldb/include/lldb/API/SBProtocolServer.h create mode 100644 lldb/source/API/SBProtocolServer.cpp create mode 100644 lldb/unittests/API/SBProtocolServerTest.cpp diff --git a/lldb/bindings/interfaces.swig b/lldb/bindings/interfaces.swig index 5cdddd5136ac2..6f09c48fba3ab 100644 --- a/lldb/bindings/interfaces.swig +++ b/lldb/bindings/interfaces.swig @@ -140,6 +140,7 @@ %include "lldb/API/SBProcessInfo.h" %include "lldb/API/SBProcessInfoList.h" %include "lldb/API/SBProgress.h" +%include "lldb/API/SBProtocolServer.h" %include "lldb/API/SBQueue.h" %include "lldb/API/SBQueueItem.h" %include "lldb/API/SBReproducer.h" diff --git a/lldb/include/lldb/API/LLDB.h b/lldb/include/lldb/API/LLDB.h index 6ac35bb4a364b..af47d12dbc404 100644 --- a/lldb/include/lldb/API/LLDB.h +++ b/lldb/include/lldb/API/LLDB.h @@ -57,6 +57,7 @@ #include "lldb/API/SBProcessInfo.h" #include "lldb/API/SBProcessInfoList.h" #include "lldb/API/SBProgress.h" +#include "lldb/API/SBProtocolServer.h" #include "lldb/API/SBQueue.h" #include "lldb/API/SBQueueItem.h" #include "lldb/API/SBReproducer.h" diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h index 5fcc685050c0b..7ec8e56067aa6 100644 --- a/lldb/include/lldb/API/SBDefines.h +++ b/lldb/include/lldb/API/SBDefines.h @@ -98,6 +98,7 @@ class LLDB_API SBProcess; class LLDB_API SBProcessInfo; class LLDB_API SBProcessInfoList; class LLDB_API SBProgress; +class LLDB_API SBProtocolServer; class LLDB_API SBQueue; class LLDB_API SBQueueItem; class LLDB_API SBReplayOptions; @@ -152,6 +153,6 @@ typedef lldb::CommandReturnObjectCallbackResult (*SBCommandPrintCallback)( typedef lldb::SBError (*SBPlatformLocateModuleCallback)( void *baton, const lldb::SBModuleSpec &module_spec, lldb::SBFileSpec &module_file_spec, lldb::SBFileSpec &symbol_file_spec); -} +} // namespace lldb #endif // LLDB_API_SBDEFINES_H diff --git a/lldb/include/lldb/API/SBProtocolServer.h b/lldb/include/lldb/API/SBProtocolServer.h new file mode 100644 index 0000000000000..625a71856aa9d --- /dev/null +++ b/lldb/include/lldb/API/SBProtocolServer.h @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_API_SBPROTOCOLSERVER_H +#define LLDB_API_SBPROTOCOLSERVER_H + +#include "lldb/API/SBDefines.h" + +namespace lldb_private { +class SBProtocolServerImpl; +} + +namespace lldb { + +/// A server that speaks a debugging protocol. +/// +/// Protocol servers are shared across debuggers. Creating a server for a +/// protocol that already has one hands back the existing server. +class LLDB_API SBProtocolServer { +public: + SBProtocolServer(); + SBProtocolServer(const SBProtocolServer &rhs); + SBProtocolServer &operator=(const SBProtocolServer &rhs); + ~SBProtocolServer(); + + /// Returns the protocol server for \p protocol, creating it if this process + /// does not already have one. On failure \p error is set. + static SBProtocolServer Create(const char *protocol, lldb::SBError &error); + + explicit operator bool() const; + bool IsValid() const; + + /// Starts listening for clients on \p connection_uri, which must be an + /// accepting URI such as "listen://[host]:port" or "accept:///path". + lldb::SBError Start(const char *connection_uri); + + lldb::SBError Stop(); + + /// Returns the URI clients can connect to, valid after a successful Start, + /// or nullptr if the server is not listening. + const char *GetConnectionURI(); + +private: + std::unique_ptr<lldb_private::SBProtocolServerImpl> m_opaque_up; +}; + +} // namespace lldb + +#endif diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt index 15c52aab41368..83ecb428d8ea4 100644 --- a/lldb/source/API/CMakeLists.txt +++ b/lldb/source/API/CMakeLists.txt @@ -81,6 +81,7 @@ add_lldb_library(liblldb SHARED ${option_framework} SBProcessInfo.cpp SBProcessInfoList.cpp SBProgress.cpp + SBProtocolServer.cpp SBQueue.cpp SBQueueItem.cpp SBReproducer.cpp diff --git a/lldb/source/API/SBProtocolServer.cpp b/lldb/source/API/SBProtocolServer.cpp new file mode 100644 index 0000000000000..7b3796599c082 --- /dev/null +++ b/lldb/source/API/SBProtocolServer.cpp @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/API/SBProtocolServer.h" +#include "lldb/API/SBError.h" +#include "lldb/Core/ProtocolServer.h" +#include "lldb/Host/Socket.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/Instrumentation.h" +#include "lldb/Utility/UriParser.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/FormatVariadic.h" + +#include <memory> + +using namespace lldb; +using namespace lldb_private; + +/// PIMPL backing SBProtocolServer. +class lldb_private::SBProtocolServerImpl { +public: + ProtocolServer *server = nullptr; +}; + +SBProtocolServer::SBProtocolServer() + : m_opaque_up(std::make_unique<SBProtocolServerImpl>()) { + LLDB_INSTRUMENT_VA(this); +} + +SBProtocolServer::SBProtocolServer(const SBProtocolServer &rhs) + : m_opaque_up(std::make_unique<SBProtocolServerImpl>(*rhs.m_opaque_up)) { + LLDB_INSTRUMENT_VA(this, rhs); +} + +SBProtocolServer &SBProtocolServer::operator=(const SBProtocolServer &rhs) { + LLDB_INSTRUMENT_VA(this, rhs); + + if (this != &rhs) + *m_opaque_up = *rhs.m_opaque_up; + return *this; +} + +SBProtocolServer::~SBProtocolServer() = default; + +SBProtocolServer SBProtocolServer::Create(const char *protocol, + SBError &error) { + LLDB_INSTRUMENT_VA(protocol, error); + + SBProtocolServer server; + error.Clear(); + + if (!protocol || !protocol[0]) { + error.SetErrorString("no protocol specified"); + return server; + } + + ProtocolServer *protocol_server = ProtocolServer::GetOrCreate(protocol); + if (!protocol_server) { + error.SetErrorStringWithFormat( + "unsupported protocol: %s. Supported protocols are: %s", protocol, + llvm::join(ProtocolServer::GetSupportedProtocols(), ", ").c_str()); + return server; + } + + server.m_opaque_up->server = protocol_server; + return server; +} + +SBProtocolServer::operator bool() const { + LLDB_INSTRUMENT_VA(this); + + return m_opaque_up->server != nullptr; +} + +bool SBProtocolServer::IsValid() const { + LLDB_INSTRUMENT_VA(this); + + return m_opaque_up->server != nullptr; +} + +SBError SBProtocolServer::Start(const char *connection_uri) { + LLDB_INSTRUMENT_VA(this, connection_uri); + + SBError error; + if (!m_opaque_up->server) { + error.SetErrorString("invalid protocol server"); + return error; + } + + const char *connection_error = + "unsupported connection specifier, expected 'accept:///path' or " + "'listen://[host]:port'"; + std::optional<URI> uri = URI::Parse(connection_uri ? connection_uri : ""); + if (!uri) { + error.SetErrorString(connection_error); + return error; + } + + std::optional<Socket::ProtocolModePair> protocol_and_mode = + Socket::GetProtocolAndMode(uri->scheme); + if (!protocol_and_mode || protocol_and_mode->second != Socket::ModeAccept) { + error.SetErrorString(connection_error); + return error; + } + + ProtocolServer::Connection connection; + connection.protocol = protocol_and_mode->first; + if (connection.protocol == Socket::SocketProtocol::ProtocolUnixDomain) + connection.name = uri->path; + else + connection.name = + llvm::formatv("[{0}]:{1}", + uri->hostname.empty() ? "0.0.0.0" : uri->hostname, + uri->port.value_or(0)) + .str(); + + if (llvm::Error err = m_opaque_up->server->Start(connection)) + error.SetErrorString(llvm::toString(std::move(err)).c_str()); + + return error; +} + +SBError SBProtocolServer::Stop() { + LLDB_INSTRUMENT_VA(this); + + SBError error; + if (!m_opaque_up->server) { + error.SetErrorString("invalid protocol server"); + return error; + } + + if (llvm::Error err = m_opaque_up->server->Stop()) + error.SetErrorString(llvm::toString(std::move(err)).c_str()); + + return error; +} + +const char *SBProtocolServer::GetConnectionURI() { + LLDB_INSTRUMENT_VA(this); + + if (!m_opaque_up->server) + return nullptr; + + Socket *socket = m_opaque_up->server->GetSocket(); + if (!socket) + return nullptr; + + std::vector<std::string> uris = socket->GetListeningConnectionURI(); + if (uris.empty()) + return nullptr; + + // A listening socket may report several equivalent URIs (e.g. IPv6 and + // IPv4 loopback). Return the first, interned so the pointer stays valid. + return ConstString(uris.front()).GetCString(); +} diff --git a/lldb/unittests/API/CMakeLists.txt b/lldb/unittests/API/CMakeLists.txt index b86054fb353f7..a7db51aea4994 100644 --- a/lldb/unittests/API/CMakeLists.txt +++ b/lldb/unittests/API/CMakeLists.txt @@ -3,6 +3,7 @@ add_lldb_unittest(APITests SBLineEntryTest.cpp SBMutexTest.cpp SBBreakpointClearConditionTest.cpp + SBProtocolServerTest.cpp SBAPITEST diff --git a/lldb/unittests/API/SBProtocolServerTest.cpp b/lldb/unittests/API/SBProtocolServerTest.cpp new file mode 100644 index 0000000000000..1191c6761e549 --- /dev/null +++ b/lldb/unittests/API/SBProtocolServerTest.cpp @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// Use the umbrella header for -Wdocumentation. +#include "lldb/API/LLDB.h" + +#include "TestingSupport/SubsystemRAII.h" +#include "lldb/API/SBDebugger.h" +#include "lldb/API/SBError.h" +#include "lldb/API/SBProtocolServer.h" +#include "gtest/gtest.h" + +using namespace lldb; +using namespace lldb_private; + +class SBProtocolServerTest : public testing::Test { +protected: + void SetUp() override { + debugger = SBDebugger::Create(/*source_init_files=*/false); + } + + void TearDown() override { SBDebugger::Destroy(debugger); } + + SubsystemRAII<lldb::SBDebugger> subsystems; + SBDebugger debugger; +}; + +TEST_F(SBProtocolServerTest, DefaultConstructedIsInvalid) { + SBProtocolServer server; + EXPECT_FALSE(server.IsValid()); + EXPECT_FALSE(static_cast<bool>(server)); +} + +TEST_F(SBProtocolServerTest, CreateMCP) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("MCP", error); + ASSERT_TRUE(error.Success()) << error.GetCString(); + EXPECT_TRUE(server.IsValid()); + EXPECT_TRUE(static_cast<bool>(server)); + // No socket is bound until Start, so there is no connection URI yet. + EXPECT_EQ(server.GetConnectionURI(), nullptr); +} + +TEST_F(SBProtocolServerTest, CreateUnsupportedProtocol) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("bogus", error); + EXPECT_TRUE(error.Fail()); + EXPECT_FALSE(server.IsValid()); +} + +TEST_F(SBProtocolServerTest, CreateNullProtocol) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create(nullptr, error); + EXPECT_TRUE(error.Fail()); + EXPECT_FALSE(server.IsValid()); +} + +TEST_F(SBProtocolServerTest, CreateEmptyProtocol) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("", error); + EXPECT_TRUE(error.Fail()); + EXPECT_FALSE(server.IsValid()); +} + +TEST_F(SBProtocolServerTest, StartAndStop) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("MCP", error); + ASSERT_TRUE(server.IsValid()); + + // Bind an ephemeral port on the loopback interface so the test never touches + // an external network. + SBError start_error = server.Start("listen://[localhost]:0"); + ASSERT_TRUE(start_error.Success()) << start_error.GetCString(); + + // Once listening, the server reports the concrete URI a client can connect + // to (the ephemeral port resolved above). + EXPECT_NE(server.GetConnectionURI(), nullptr); + + EXPECT_TRUE(server.Stop().Success()); +} + +TEST_F(SBProtocolServerTest, StartInvalidServer) { + // A default-constructed server has no backing protocol server, so Start fails + // before the connection URI is ever examined. + SBProtocolServer server; + EXPECT_TRUE(server.Start("").Fail()); +} + +TEST_F(SBProtocolServerTest, StopNotRunning) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("MCP", error); + ASSERT_TRUE(server.IsValid()); + // Stopping a server that was never started is an error. + EXPECT_TRUE(server.Stop().Fail()); +} + +TEST_F(SBProtocolServerTest, StartRejectsNonAcceptingURI) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("MCP", error); + ASSERT_TRUE(server.IsValid()); + // A connecting URI (not an accepting one) is rejected before any socket work. + EXPECT_TRUE(server.Start("connect://localhost:1234").Fail()); + EXPECT_TRUE(server.Start("not a uri").Fail()); +} + +TEST_F(SBProtocolServerTest, CopyReferencesSameServer) { + SBError error; + SBProtocolServer server = SBProtocolServer::Create("MCP", error); + ASSERT_TRUE(server.IsValid()); + + // The protocol server is owned by the engine, not by SBProtocolServer, so a + // copy just references the same server and the source stays valid. + SBProtocolServer copy(server); + EXPECT_TRUE(copy.IsValid()); + EXPECT_TRUE(server.IsValid()); + + // Copy assignment behaves the same way. + SBProtocolServer assigned; + EXPECT_FALSE(assigned.IsValid()); + assigned = server; + EXPECT_TRUE(assigned.IsValid()); + EXPECT_TRUE(server.IsValid()); +} _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
