.NET Diagnostic IPC protocol: the C++ way

Christophe Nasarre
4 min readSep 18, 2022

--

The previous post was describing the C# helpers to communicate with the diagnostic server in the CLR of a running .NET application.

If, like me, you must write native code (i.e not in C#), you will need to implement the transport and protocol yourself. And, as you will see, it is not that complicated thanks to the documentation but also by using the available C# code of the Microsoft.Diagnostics.NETCore.Client implementation as a guide.

EventPipe transport layer

The first step is to connect to the CLR of a running .NET process. On Linux, you connect to a domain socket named “{$TMPDIR}/dotnet-diagnostic-{%d:PID}-{%llu:disambiguation key}-socket”. For Windows, a named pipe called \\.\pipe\dotnet-diagnostic-{%d:PID} needs to be accessed.

Here is the Windows implementation code to connect to the IPC named pipe:

Dig into the Command protocol

Once the connection is created, a command can be sent by writing to the pipe (or socket on Linux). The answer will be received by reading from the pipe (or socket on Linux). There is something important to remember: if you need to send different commands, it is needed to create one connection for each. You should not try to reuse a given connection that might also be closed after a command has been processed.

Let’s start with the IpcHeader

Both the command and the response share the same header format:

The Size field stores the size of the header (= 20) plus the size of the payload (if any). Next comes the CommandSet field that identifies the groups in which the command belongs to:

It is then followed by the ID of a command in that CommandSet :

For example, here is the memory layout of a ProcessInfo command:

Since there is no additional parameter that would need to be encoded in a payload, the Size field is set to 20 (= 0x14 in hexadecimal) which is the size of the header alone.

To make command handling easier, I have defined the corresponding headers:

All that makes the the code to send such a ProcessInfo command straightforward:

The next step is to read from the pipe to get the answer from the CLR. As mentioned earlier, the same IpcHeader is received; always with the Server (0xFF) CommandSet value. The CommandId field is 0 for a success and 0xFF in case of an error.

For example, here is the memory layout of a ProcessInfo answer:

Note that numbers are little-endian encoded (hence the 0001 for the Size field: it means 0x0100 = 256 bytes)

The code to analyze the response follows:

In case of success, the size of the response payload is obtained from the Size field by subtracting the size of the header. The next step is to allocate a buffer and read the payload from the pipe into that buffer:

How to access the response fields

Now that the response payload has been read into a memory buffer, it is time to look at the expected fields as described in the documentation. Here is the encoding of the different field types:

To continue with the ProcessInfo example, here are the expected fields:

So here is the memory layout for the response payload for my monitored Windows 64-bit simulator.exe test application:

To simplify the implementation, the class in charge of a command allocates a buffer corresponding to the payload and provides fields with values either copied from it or, in case of strings, pointing to the buffer (just after the 32-bit length):

The PointToString helper code is straightforward:

You now know how to send a command without parameter and analyze the expected response. The next post will show you how to listen to CLR events like dotnet trace.

Resources

--

--

Christophe Nasarre
Christophe Nasarre

Written by Christophe Nasarre

Loves to understand how things work (MVP Developer Technologies)

No responses yet