Skip to content

Commit 3d29af3

Browse files
committed
[lldb][RPC] Design doc for upstreaming PR
This mainly adds as design doc to help follow with the current PRs up for upstreaming the `lldb-rpc-gen` tool and emitters.
1 parent 76d83e6 commit 3d29af3

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed

lldb/docs/rpc-design-doc.rst

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
LLDB RPC Upstreaming Design Doc
2+
===============================
3+
4+
This document aims to explain the general structure of the upstreaming patches for adding LLDB RPC. The 2 primary concepts explained here will be:
5+
6+
* How LLDB RPC is used
7+
* How the ``lldb-rpc-gen`` works and what it outputs
8+
9+
LLDB RPC
10+
*********
11+
12+
LLDB RPC is a framework by which processes can communicate with LLDB out of process while maintaining compatibility with the SB API. More details are explained in the `RFC<https://discourse.llvm.org/t/rfc-upstreaming-lldb-rpc/85804>`_ for upstreaming LLDB RPC, but the main focus in this doc for this section will be how exactly the code is structured for the PRs that will upstream this code.
13+
14+
The ``lldb-rpc-gen`` tool
15+
*************************
16+
17+
``lldb-rpc-gen`` is the tool that generates the main client and server interfaces for LLDB RPC. It is a ``ClangTool`` that reads all SB API header files and their functions and outputs the client/server interfaces and certain other pieces of code, such as RPC-specfic versions of Python bindings used for the test suite. There's 3 main components behind ``lldb-rpc-gen``:
18+
19+
1. The ``lldb-rpc-gen`` tool itself, which contains the main driver that uses the ``ClangTool``.
20+
2. The code that generates all interfaces, which we call "emitters". All generated code for the interfaces are in C++, so the server side has one emitter for its generated source code and another for its generated header code. The client side has the same.
21+
3. All common code shared between all emitters, such as helper methods and information about exceptions to take when emitting.
22+
23+
There are currently 2 PRs up for upstreaming RPC:
24+
- One that adds the ``lldb-rpc-gen`` tool and its common code: https://github.com/llvm/llvm-project/pull/138031
25+
- One that adds the RPC client-side interface code emitters: https://github.com/llvm/llvm-project/pull/147655
26+
27+
The `current PR<https://github.com/llvm/llvm-project/pull/136748>`_ up for upstreaming LLDB RPC upstreams a subset of the code used for the tool. It upstreams the ``lldb-rpc-gen`` tool and all code needed for the server side emitters. Here's an example of what ``lldb-rpc-gen`` will output for the server side interface:
28+
29+
Input
30+
-----
31+
32+
We'll use ``SBDebugger::CreateTarget(const char *filename)`` as an example. ``lldb-rpc-gen`` will read this method from ``SBDebugger.h``. The output is as follows.
33+
34+
Server-side Output
35+
******************
36+
37+
Source Code Output
38+
------------------
39+
40+
Server-side Source Code
41+
~~~~~~~~~~~~~~~~~~~~~~~
42+
::
43+
44+
bool rpc_server::_ZN4lldb10SBDebugger12CreateTargetEPKc::HandleRPCCall(rpc_common::Connection &connection, RPCStream &send, RPCStream &response) {
45+
// 1) Make local storage for incoming function arguments
46+
lldb::SBDebugger *this_ptr = nullptr;
47+
rpc_common::ConstCharPointer filename;
48+
// 2) Decode all function arguments
49+
this_ptr = RPCServerObjectDecoder<lldb::SBDebugger>(send, rpc_common::RPCPacket::ValueType::Argument);
50+
if (!this_ptr)
51+
return false;
52+
if (!RPCValueDecoder(send, rpc_common::RPCPacket::ValueType::Argument, filename))
53+
return false;
54+
// 3) Call the method and encode the return value
55+
lldb::SBTarget && __result = this_ptr->CreateTarget(filename.c_str());
56+
RPCServerObjectEncoder(response, rpc_common::RPCPacket::ValueType::ReturnValue, std::move(__result));
57+
return true;
58+
}
59+
60+
Function signature
61+
~~~~~~~~~~~~~~~~~~
62+
63+
All server-side source code functions have a function signature that take the format ``bool rpc_server::<mangled-function-name>::HandleRPCCall(rpc_common::Connection &connection, RPCStream &send, RPCStream &response)``. The mangled name is used in order to differentiate between overloaded methods. Here the ``connection`` is what's maintained between the client and server. The ``send`` variable is a byte stream that carries information sent from the client. ``response`` is also a byte stream that will be populated with the return value obtained from the call into the SB API function that will be sent back to the client.
64+
65+
For the client-side sources, the function signature is identical to that of what the signature looks like in the main SB API.
66+
67+
Local variable storage
68+
~~~~~~~~~~~~~~~~~~~~~~
69+
70+
First, variables are created to hold all arguments coming in from the client side. These variables will be a pointer for the SB API class in question, and corresponding variables for all parameters that the function has. Since this signature for ``SBDebugger::CreateTarget()`` only has one parameter, a ``const char *``, 2 local variables get created. A pointer for an ``SBDebugger`` object, and an ``RPCCommon::ConstCharPointer`` for the ``const char * filename`` parameter. The ``ConstCharPointer`` is a class backed by ``std::string`` in the main RPC core code.
71+
72+
Incoming stream decoding
73+
~~~~~~~~~~~~~~~~~~~~~~~~
74+
75+
Following this, ``RPCServerObjectDecoder`` is used to decode the ``send`` byte stream. In this case, we're decoding this stream into the ``SBDebugger`` pointer we created earlier. We then decode the ``send`` stream again to obtain the ``const char * filename`` sent by the client. Each decoded argument from the client is checked for validity and the function will exit early if any are invalid.
76+
77+
SB API function call
78+
~~~~~~~~~~~~~~~~~~~~
79+
80+
Once all arguments have been decoded, the underlying SB API function called with the decoded arguments. ``RPCServerObjectEncoder`` is then used to encode the return value from the SB API call into the ``response`` stream, and this is then sent back to the client.
81+
82+
Header Code Output
83+
------------------
84+
::
85+
86+
class _ZN4lldb10SBDebugger12CreateTargetEPKc : public rpc_common::RPCFunctionInstance {
87+
public:
88+
_ZN4lldb10SBDebugger12CreateTargetEPKc() : RPCFunctionInstance("_ZN4lldb10SBDebugger12CreateTargetEPKc") {}
89+
~_ZN4lldb10SBDebugger12CreateTargetEPKc() override {}
90+
bool HandleRPCCall(rpc_common::Connection &connection, rpc_common::RPCStream &send, rpc_common::RPCStream &response) override;
91+
};
92+
93+
Class definition and ``HandleRPCCall``
94+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
95+
96+
All RPC server side functions are subclasses of ``RPCFunctionInstance``. Each class will then define their ``HandleRPCCall`` function that is seen in the source code above. This subclassing and ``HandleRPCCall`` definition is what is emitted in the header code for server.
97+
98+
Client-side Output
99+
******************
100+
101+
Client-side Source Code
102+
~~~~~~~~~~~~~~~~~~~~~~~
103+
::
104+
105+
lldb_rpc::SBTarget lldb_rpc::SBDebugger::CreateTarget(const char * filename) {
106+
// 1) Perform setup
107+
// Storage for return value
108+
lldb_rpc::SBTarget __result = {};
109+
// Deriving connection from this.
110+
rpc_common::ConnectionSP connection_sp = ObjectRefGetConnectionSP();
111+
if (!connection_sp) return __result;
112+
// RPC Communication setup
113+
static RPCFunctionInfo g_func("_ZN4lldb10SBDebugger12CreateTargetEPKc");
114+
RPCStream send;
115+
RPCStream response;
116+
g_func.Encode(send);
117+
RPCValueEncoder(send, rpc_common::RPCPacket::ValueType::Argument, *this);
118+
RPCValueEncoder(send, rpc_common::RPCPacket::ValueType::Argument, filename);
119+
// 2) Send RPC call
120+
if (!connection_sp->SendRPCCallAndWaitForResponse(send, response))
121+
return __result;
122+
// 3) Decode return values
123+
RPCValueDecoder(response, rpc_common::RPCPacket::ValueType::ReturnValue, __result);
124+
return __result;
125+
}
126+
127+
Function signature
128+
~~~~~~~~~~~~~~~~~~
129+
130+
For the client-side sources, the function signature is almost always identical to that of what the
131+
signature looks like in the main SB API, with the namespace changing from ``lldb`` to ``lldb_rpc``. For some methods, the function signature might need to change to prepend an RPC connection as the first argument. This happens in the event that the function is static. Since RPC functions usually derive their connection from their instance, static functions must be given a connection as they have no instance to derive one from.
132+
133+
Return Value Storage
134+
~~~~~~~~~~~~~~~~~~~~
135+
136+
We first need to create storage for the return value. For this method we return an ``SBTarget``, so we need to create an ``lldb_rpc::SBTarget`` for the return value and initialize it to an empty value. Since we are on the client-side, all instances where we use SB API classes will be from the ``lldb_rpc`` namespace.
137+
138+
Obtaining RPC Connection
139+
~~~~~~~~~~~~~~~~~~~~~~~~
140+
141+
We then need to obtain the RPC connection. In this case, we obtain the connection by deriving from the ``SBDebugger`` instance that would've been created prior to this function call by using ``ObjectRefGetConnectionSP()``. If this connection is invalid then we return an empty value.
142+
143+
Encoding RPC Stream Information
144+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
145+
146+
Using the mangled function name for the function information, we encode the streams used to send the function's parameters and receive the return info from the server-side call. For this function, we need to encode the pointer to the ``SBDebugger()`` instance itself, and the ``const char *filename*`` parameter as well.
147+
148+
Sending Encoded Information to Server
149+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
150+
151+
Once the information has been encoded, ``SendRPCCallAndWaitForResponse(send, response)`` is used to send the information to the server-side, where the underlying call to ``lldb::SBDebugger::CreateTarget(const char **)`` will be made. If this call failed, then an empty value will be returned.
152+
153+
Decoding Information Received from Server
154+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
155+
156+
If the call to send the encoded information was successful, then we need to decode what the server sent in response. This decoded value is then placed in the return value storage that we set up earlier and is then returned to the connected client itself.
157+
158+
Header Code Output
159+
------------------
160+
161+
::
162+
lldb_rpc::SBTarget CreateTarget(const char * filename);
163+
164+
The output for the header code on the client-side is significantly simpler. Similar to the function signature for the client-side sources, the header file output is just the function signature for each method. This means that it will match the original SB API function signature, with the only exceptions being for static methods as stated above for the source code function signature.
165+
166+
``lldb-rpc-gen`` emitters
167+
*****************************
168+
169+
The bulk of the code is generated using the emitters. For the server side, we have ``RPCServerSourceEmitter`` and ``RPCServerHeaderEmitter``. The former handles generation of the source code and the latter handles generation of the header code seen above. Similarly for the client side, we have ``RPCLibrarySourceEmitter`` and ``RPCLibraryHeaderEmitter``. Similar the server emitters, these emitter handle generating the source and header code for the client side, respectively.
170+
171+
Emitters largely have similar structure. Constituent sections of code, such as function headers, function bodies and others are typically given their own method. As an example, the function to emit a function header is ``EmitFunctionHeader()`` and the function to emit a function body is ``EmitFunctionBody()``. Information that will be written to the output file is written using ``EmitLine()``, which uses ``llvm::raw_string_ostreams`` and is defined in ``RPCCommon.h``.
172+
173+
Since this is a ``ClangTool``, information about each method is obtained from Clang itself and stored in the ``Method`` struct located in ``RPCCommon.h`` in ``lldb-rpc-gen``'s directory. ``Method`` is used for simplicity and abstraction, and other information that would be needed from the SB API is obtained from Clang directly.
174+
175+
Testing
176+
*******
177+
178+
The RPC interface and the code emitters are tested in 2 main ways:
179+
180+
- The RPC client and server interfaces are tested by running the full LLDB SB API test suite against liblldbrpc. Using this, Python acts as the client binary connecting to RPC, and all SB API calls from API tests will go through the RPC client/server flow as described above. This has its own ninja target, ``check-lldb-rpc``.
181+
- The RPC client and server emitters are tested using shell tests where FileCheck checks the output of the emitters against a set of heuristics that we have. Currently, these shell tests exist for the client side emitters as they have more heuristics than the server-side emitters.

0 commit comments

Comments
 (0)