[lldb] Add SBProtocolServer to start protocol servers programmatically#209923
Merged
Conversation
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
|
@llvm/pr-subscribers-lldb Author: Jonas Devlieghere (JDevlieghere) ChangesStarting a protocol server (such as MCP, and in the future potentially DAP) is only possible from the command line today, via 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 Full diff: https://github.com/llvm/llvm-project/pull/209923.diff 8 Files Affected:
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());
+}
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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