RDP DVC advanced plugin – C++
A complete native C++ sample demonstrating Remote Desktop Protocol Dynamic Virtual Channels (DVC), including client plugins, a server application, COM activation models, and client-side bitmap rendering.
This sample implements:
- RDP Dynamic Virtual Channel client plugins
- A server application running inside an RDP session
- Three different COM activation models
- Protocol Buffers messaging
- RDP render hints and client-side bitmap rendering
The system measures round-trip latency by sending ping messages across the virtual channel and echoing them back. The bitmap renderer generates the scrolling rainbow pattern to demonstrate client-side injection of bitmaps into the RDP client graphics pipeline.
Quick Facts
- Channel:
dvc::sample::advancedplugin - CLSID:
{D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3}(shared by all client plugins) - Client outputs:
- LocalServer EXE:
bin/<Platform>/<Configuration>/rdp-plugin-localserver-cpp.exe - InprocServer DLL:
bin/<Platform>/<Configuration>/rdp-plugin-inprocserver-cpp.dll - LoadLibrary DLL:
bin/<Platform>/<Configuration>/rdp-plugin-loadlibrary-cpp.dll
- LocalServer EXE:
- Server output:
bin/<Platform>/<Configuration>/rdp-plugin-server-cpp.exe - Register:
- LocalServer:
/register(or/register /machinefor machine-wide) - Inproc:
regsvr32 rdp-plugin-inprocserver-cpp.dll - LoadLibrary:
regsvr32 rdp-plugin-loadlibrary-cpp.dll(exportsVirtualChannelGetInstance)
- LocalServer:
- Server modes: WTS (sync) or file-handle (async)
Table of Contents
- Overview
- Quick Facts
- Quick Start
- Contents
- Prerequisites
- Setup
- Architecture
- RDP Graphics APIs
- Shared Infrastructure
- Client Plugins
- Server Application
- Plugin Registration
- Running the sample
- C++-Specific Considerations
- Thread Safety
- What This Demonstrates
- Next steps
- License
Quick Start
Install prerequisites (Visual Studio Build Tools, vcpkg)
# From this directory (Advanced\cpp)
.\build.ps1 -Install
Build All Projects
# From this directory (Advanced\cpp)
.\build.ps1
# Or build specific configuration
.\build.ps1 -Configuration Release -Platform x64
# Or build specific project
.\build.ps1 -Project server -Configuration Debug
Test the Implementation
- Choose and register a client plugin (see Plugin Registration)
- Run the server inside an RDP session:
bin\x64\Debug\rdp-plugin-server-cpp.exe - Connect via RDP client to the session
- Observe:
- Server logs RTT measurements
- Client plugin echoes data back
- Bitmap locally rendered and injected into the RDP client window
Contents
| File/folder | Description |
|---|---|
rdp-plugin-common/ |
Shared plugin infrastructure (static lib). |
rdp-plugin-protocol/ |
Protocol Buffers message definitions (static lib). |
rdp-plugin-inprocserver/ |
In-process COM server plugin (DLL). |
rdp-plugin-loadlibrary/ |
LoadLibrary/VirtualChannelGetInstance plugin (DLL). |
rdp-plugin-localserver/ |
Out-of-process COM server plugin (EXE). |
rdp-plugin-server/ |
Server-side DVC host application (EXE). |
RdpDvcAdvanced.sln |
Visual Studio solution file. |
build.ps1 |
PowerShell build script. |
README.md |
This README file. |
This directory contains native C++ implementations showcasing three different COM activation models for RDP client plugins, plus a server-side DVC host application and shared infrastructure.
Key Features
- Native C++ with C++/WinRT for modern COM usage
- Three COM activation models (InprocServer32, LoadLibrary, LocalServer32)
- Protocol Buffers via vcpkg manifest mode
- Render hints + bitmap renderer demonstrating the full RDP graphics pipeline for media offloading
- Two server I/O modes: synchronous WTS API and asynchronous file-handle (OVERLAPPED)
Shared Infrastructure
rdp-plugin-common - Shared library (static .lib)
- Common plugin implementation (
IWTSPlugin,IWTSListenerCallback,IWTSVirtualChannelCallback) - COM class factory utilities
- Logging and registry helpers
- Linked into all client plugins
- Common plugin implementation (
rdp-plugin-protocol - Protocol library (static .lib)
- Protocol Buffers message definitions and helpers
- Message framing (length-prefix + protobuf payload)
- Ping/pong message construction and parsing
- Shared by both client and server
Client Plugins (3 Activation Models)
rdp-plugin-inprocserver - In-process COM server (DLL)
- Loaded by the RDP client via
CoCreateInstance - Implements
DllGetClassObjectfor COM registration
- Loaded by the RDP client via
rdp-plugin-loadlibrary - Direct LoadLibrary (DLL)
- Exports
VirtualChannelGetInstancefunction - Alternative to COM activation
- Loaded directly by RDP client stack
- Requires registration (registers DLL path in AddIns registry key)
- Exports
rdp-plugin-localserver - Out-of-process COM server (EXE)
- Runs as separate process
- Provides process isolation from the RDP client
- Demonstrates LocalServer32 registration
- Easier debugging and independent lifecycle
Server Application
- rdp-plugin-server - Server-side DVC host (EXE)
- Runs inside RDP session on remote machine
- Opens DVC channel to communicate with client plugins
- Demonstrates session lifecycle, reconnect handling, render hints
- Two I/O modes: WTS API (sync) and File Handle (async)
Prerequisites
- Visual Studio 2022 or later (or Build Tools) with C++ workload
- Windows SDK 10.0.17763.0 or later
- vcpkg component (included with Visual Studio or installed separately)
- Protocol Buffers (automatically installed via vcpkg manifest mode)
Run .\build.ps1 -Install to automatically install Visual Studio Build Tools with required C++ components and vcpkg via winget.
Setup
vcpkg Manifest Mode
This solution uses vcpkg manifest mode for automatic dependency management:
- vcpkg.json - Declares dependencies (protobuf)
- Directory.Build.props - Enables vcpkg manifest integration for all projects
Build Commands
Local Build (this directory):
# Auto-detect platform, build all projects in Debug
.\build.ps1
# Install prerequisites (Visual Studio Build Tools, vcpkg)
.\build.ps1 -Install
# Specific configuration and platform
.\build.ps1 -Configuration Release -Platform ARM64
# Static vs Dynamic linking (default: static)
.\build.ps1 -ProtobufLink:dynamic # Use dynamic CRT (/MD)
.\build.ps1 -ProtobufLink:static # Use static CRT (/MT) - default
# Clean build outputs
.\build.ps1 -Clean
# Force rebuild (clean first)
.\build.ps1 -Force
# Build specific project
.\build.ps1 -Project common # Build rdp-plugin-common only
.\build.ps1 -Project protocol # Build rdp-plugin-protocol only
.\build.ps1 -Project server # Build server application
.\build.ps1 -Project inprocserver # Build InprocServer plugin
.\build.ps1 -Project loadlibrary # Build LoadLibrary plugin
.\build.ps1 -Project localserver # Build LocalServer plugin
The first build will take longer as vcpkg downloads and builds protobuf and its dependencies (abseil, utf8_range, etc.).
Build Outputs
Binaries are placed in:
bin\{Platform}\{Configuration}\- Executables and DLLsobj\{ProjectName}\{Platform}\{Configuration}\- Intermediate files
Troubleshooting
If you see "protoc.exe not found":
- Delete
vcpkg_installed\folder - Run
.\build.ps1 -Clean - Rebuild the solution - vcpkg will reinstall dependencies
Architecture
Client Plugin Flow
RDP Client
├─> Loads plugin DLL or creates a COM server
├─> Calls IWTSPlugin::Initialize
├─> Listens for DVC connections on "dvc::sample::advancedplugin"
├─> IWTSListenerCallback::OnNewChannelConnection
├─> IWTSVirtualChannelCallback receives data
├─> Echoes data back to server
└─> IWTSPlugin::Terminated on shutdown
Server Application Flow
RDP Session (remote machine)
├─> Registers for session notifications (WTSRegisterSessionNotificationEx)
├─> Opens DVC channel (WTSVirtualChannelOpenEx)
├─> Sends ping messages with render hints
├─> Receives echo responses, measures RTT
├─> Handles session disconnect/reconnect
└─> Graceful shutdown on exit events
Protocol
All implementations use Protocol Buffers for message serialization:
Message Structure:
[4-byte length prefix (little-endian)]
[Protocol Buffer payload (PingMessage)]
PingMessage fields:
sequence_number- Incrementing message counterutc_ticks- Timestamp (100ns ticks since Unix epoch)payload- Data bytes (echo test)render_hint_id- Window/region identifier for RDP optimizationrender_hint_type- Hint type (e.g., RENDER_HINT_MAPPEDWINDOW)
RDP Graphics APIs
Render Hints (Server-Side)
Purpose: Associate application windows with RDP protocol regions to optimize encoding and transmission. The server creates render hints that inform the RDP host about content regions requiring special handling.
API: wtshintapi.h - WTSSetRenderHint()
Implementation in rdp-plugin-server:
#include <wtshintapi.h>
// Create a render hint for a window region
ULONGLONG hintId = 0;
RECT hintRect = { left, top, right, bottom };
int hr = WTSSetRenderHint(
&hintId, // [in/out] Hint ID (0 = create new)
hWnd, // Associated window handle
RENDER_HINT_MAPPEDWINDOW, // Hint type
sizeof(hintRect), // Data size
reinterpret_cast<BYTE*>(&hintRect)); // Hint data (RECT)
// Clear a render hint
WTSSetRenderHint(&hintId, hWnd, RENDER_HINT_CLEAR, 0, nullptr);
Hint Types:
RENDER_HINT_MAPPEDWINDOW- Associates a rectangular region with a windowRENDER_HINT_CLEAR- Removes a previously set hint
Server Workflow:
- Create a "render window" to anchor the hint
- Calculate hint rectangle (e.g., 80% of client area, centered)
- Call
WTSSetRenderHint()withRENDER_HINT_MAPPEDWINDOW - Include the returned
hintIdin ping messages to client - Clear hint before window destruction or on disconnect
Thread Safety: Render hint operations are protected by mutex (m_renderHintLock) since hint ID must be atomically created and stored.
Bitmap Renderer Context (Client-Side)
Purpose: Render custom bitmap content directly into the RDP client window, overlaying a region that the server has designated via a render hint. The client-side bitmap renderer runs in the RDP client process context and is only available while an RDP session is connected.
What is the "context": Each IWTSBitmapRenderer represents a rendering context for one specific mapped region. The context is tied to the render hint ID issued by the server — that ID is the key linking the server's window region to the client's render surface. The client draws frames into this context; the RDP stack composites them onto the corresponding part of the remote desktop display.
API: tsvirtualchannels.h - IWTSBitmapRenderService, IWTSBitmapRenderer, IWTSBitmapRendererCallback
Documentation Links:
- IWTSBitmapRenderService: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderservice
- IWTSBitmapRenderer: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderer
- IWTSBitmapRendererCallback: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderercallback
Acquiring the Service (in IWTSPlugin::Initialize):
The bitmap render service is obtained by querying IWTSPluginServiceProvider (via QI on the IWTSVirtualChannelManager passed to Initialize). It is only available on supported RDP client builds.
// QI for IWTSPluginServiceProvider from the channel manager
com_ptr<IWTSPluginServiceProvider> serviceProvider;
channelManager->QueryInterface(IID_IWTSPluginServiceProvider, serviceProvider.put_void());
// Get the bitmap render service
com_ptr<IUnknown> service;
serviceProvider->GetService(RDCLIENT_BITMAP_RENDER_SERVICE, service.put());
com_ptr<IWTSBitmapRenderService> bitmapService;
service->QueryInterface(IID_IWTSBitmapRenderService, bitmapService.put_void());
Creating a Renderer Context (for a given hint ID):
// Create renderer for a specific mapping ID (from server's render_hint_id)
com_ptr<IWTSBitmapRenderer> renderer;
bitmapService->GetMappedRenderer(
mappingId, // Render hint ID received from server via DVC message
this, // IWTSBitmapRendererCallback for size-change notifications
renderer.put());
// Render a frame: BGRA32 pixel data, bottom-up row order (standard Windows DIB)
HRESULT hr = renderer->Render(
GUID_NULL, // Reserved, pass GUID_NULL
width, height, // Frame dimensions in pixels
stride, // Row stride in bytes (width * 4 for BGRA32)
(DWORD)bitmapSize, // Total buffer size in bytes
bitmapData); // Pointer to BGRA32 pixel buffer
// Remove renderer when done (e.g., on disconnect or channel close)
renderer->RemoveMapping();
Pixel Format: BGRA32 (4 bytes per pixel: Blue, Green, Red, Alpha), bottom-up row order. The stride is width * 4. The render surface dimensions are determined by the server's hint rectangle and are communicated to the plugin via IWTSBitmapRendererCallback::OnTargetSizeChanged.
BitmapRendererManager Class:
The BitmapRendererManager class in rdp-plugin-common provides:
- Renderer Pool: Persistent collection of renderers that survive disconnect/reconnect
- Per-Renderer Threads: Each renderer runs in its own thread at 30fps
- Rainbow Animation: Demo content showing scrolling rainbow pattern
- Pause/Resume: Stop rendering on disconnect, resume on reconnect
- Thread-Safe: Mutex-protected pool, atomic pause flag, lock-free size updates
Key Methods:
// Handle render hint from server message
void HandleRenderHint(ULONGLONG renderHintId);
// Lifecycle management
void OnDisconnected(); // Pause all rendering
void OnConnected(); // Resume all rendering
void CleanupAllRenderers(); // Final cleanup
// IWTSBitmapRendererCallback
HRESULT OnTargetSizeChanged(RECT rcNewSize); // Called when target resizes
Renderer Thread Lifecycle:
HandleRenderHint()called with hint ID from server message- Get or create renderer from pool for that mapping ID
- Marshal
IWTSBitmapRendererinterface to new thread (COM apartment safety) - Thread runs render loop: check pause flag → generate frame →
Render()→ sleep - On removal: signal exit, join thread, remove from pool
Memory Efficiency:
- Reusable bitmap buffer per renderer (allocation-free rendering)
- Pool-based renderer management (no per-frame allocations)
- Atomic pause flag (no mutex on hot path)
End-to-End Flow
Server (rdp-plugin-server) Client (rdp-plugin-inprocserver)
───────────────────────── ──────────────────────────────
1. Create render window
2. Calculate hint rect (80% centered)
3. WTSSetRenderHint(MAPPEDWINDOW)
4. Get hintId back
│
├─── PingMessage { render_hint_id } ────────>
│ 5. Parse render_hint_id
│ 6. BitmapRendererManager.HandleRenderHint(id)
│ 7. GetMappedRenderer(id) creates renderer
│ 8. Render thread starts (30fps rainbow)
│
<──── EchoMessage (same content) ───────────
│
[window resize]
9. UpdateRenderHintFromClientRect()
10. WTSSetRenderHint (update rect)
│
├─── PingMessage { same hint_id } ─────────>
│ 11. OnTargetSizeChanged(newRect)
│ 12. Render adapts to new size
│
[disconnect]
13. ClearRenderHint()
14. Close channel 15. OnDisconnected() - pause rendering
│ (renderers kept in pool)
│
[reconnect]
15. Open channel
16. New WTSSetRenderHint() → new hintId
│
├─── PingMessage { new_hint_id } ──────────>
│ 17. OnConnected() - resume rendering
│ 18. Create new renderer for new ID
│ (old renderers cleaned up)
Shared Infrastructure
rdp-plugin-common Library
Purpose: Reusable components for building RDP client plugins that communicate via Dynamic Virtual Channels. Eliminates code duplication across different COM activation models.
Core Features:
- RdpPlugin class: Complete implementation of all required RDP plugin interfaces
- BitmapRendererManager class: Manages bitmap renderers with dedicated threads
- COM infrastructure: Generic
IClassFactory, thread-safe reference counting - Utilities: Logging helpers, registry helpers, RAII wrappers
Interfaces Implemented:
IWTSPlugin- Plugin lifecycle (Initialize, Terminated)IWTSListenerCallback- New channel notifications (OnNewChannelConnection)IWTSVirtualChannelCallback- Data I/O events (OnDataReceived, OnClose)IWTSBitmapRendererCallback- Target size change notifications
Key Methods:
Initialize(IWTSVirtualChannelManager* pChannelMgr)
- Called by RDP client to initialize plugin
- Creates listener for "dvc::sample::advancedplugin" channel
- Returns S_OK on success
OnNewChannelConnection(IWTSVirtualChannel* pChannel, ...)
- Called when server opens channel from remote session
- Stores channel pointer for Write operations
- Returns IWTSVirtualChannelCallback interface (this)
OnDataReceived(ULONG cbSize, BYTE* pBuffer)
- Called when data arrives from server
- Reassembles fragmented messages
- Parses framed protobuf message
- Echoes data back via IWTSVirtualChannel::Write
ComClassFactory Class:
- Generic
IClassFactoryimplementation for creating COM objects - Thread-safe reference counting
- Module lock management (prevents DLL unload while objects exist)
- Proper
QueryInterfacewith interface validation
rdp-plugin-protocol Library
Purpose: Common message format and serialization layer for communication between RDP client plugins and server-side applications. Ensures protocol compatibility across different programming languages.
Message Definitions:
syntax = "proto3";
package rdp.plugin.protocol;
message PingMessage {
uint32 sequence_number = 1; // Incrementing counter for tracking
int64 utc_ticks = 2; // Timestamp (100ns since Unix epoch)
bytes payload = 3; // Data bytes for echo testing
uint64 render_hint_id = 4; // Window/region identifier
uint32 render_hint_type = 5; // Hint type (e.g., MAPPEDWINDOW)
}
message FrameHeader {
uint32 data_length = 1;
}
Framing Format:
Offset Bytes Description
------ -------- -----------
0x0000 0C 02 00 00 Length = 524 (0x020C) little-endian
0x0004 08 01 10 ... Protobuf payload (524 bytes)
PingMessageHelper Class:
Constants:
static constexpr std::size_t FRAME_HEADER_SIZE = 4; // Length prefix size
static constexpr std::size_t DEFAULT_PAYLOAD_SIZE = 5u * 1024u; // 5 KB default
static constexpr std::size_t MAX_PAYLOAD_SIZE = 16u * 1024u * 1024u; // 16 MB limit
Key Methods:
CreatePingMessage()- Factory with default payload generationSerializeWithFraming()- Adds length prefix to protobuf bytesTryParseFramed()- Validates and parses framed messages
Client Plugins
rdp-plugin-inprocserver
Activation: In-process COM server registered with InprocServer32
When to use:
- Production deployments requiring maximum performance
- Tight integration with RDP client process
- Standard Windows COM activation pattern
COM Entry Points:
HRESULT STDAPICALLTYPE DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv);
HRESULT STDAPICALLTYPE DllCanUnloadNow(void);
HRESULT STDAPICALLTYPE DllRegisterServer(void);
HRESULT STDAPICALLTYPE DllUnregisterServer(void);
Loading Sequence:
- RDP client reads AddIns registry key → finds CLSID
{D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3} - RDP client calls
CoCreateInstance(CLSID, ...) - COM looks up CLSID in registry → finds InprocServer32 path
- COM calls
LoadLibrary("rdp-plugin-inprocserver.dll") - COM calls
DllGetClassObject()→ getsIClassFactory - COM calls
factory->CreateInstance()→ creates RdpPlugin - RDP client calls
plugin->Initialize()→ creates listener
rdp-plugin-loadlibrary
Activation: Direct DLL loading via VirtualChannelGetInstance export
Entry Point:
HRESULT VCAPITYPE VirtualChannelGetInstance(
REFIID refiid, // Requested interface IID
ULONG* pNumObjs, // [in/out] Object count
VOID** ppObjArray // [out] Array of interface pointers
);
Two-Phase Call Pattern:
Phase 1 - Discovery (pNumObjs != NULL, ppObjArray == NULL):
ULONG count = 0;
hr = VirtualChannelGetInstance(IID_IWTSPlugin, &count, NULL);
// Returns: S_OK, count = 1
Phase 2 - Creation (pNumObjs != NULL, ppObjArray != NULL):
ULONG count = 1; // Reuse count from Phase 1
IWTSPlugin* plugin = NULL;
hr = VirtualChannelGetInstance(IID_IWTSPlugin, &count, (VOID**)&plugin);
// Returns: S_OK, plugin = new RdpPlugin instance
rdp-plugin-localserver (Process Isolation) (Recommended)
Activation: Out-of-process COM server (standalone EXE)
- Development and testing (easier debugging)
- Process isolation from RDP client
- Separate privilege requirements
- Independent lifecycle from client
COM LocalServer Pattern:
- COM launches EXE when first client calls
CoCreateInstance - EXE registers class factory via
CoRegisterClassObject - COM creates plugin instance via factory (cross-process marshaling)
- EXE stays running while plugin objects exist
- EXE revokes class factory and exits when no more clients
Process Lifetime:
- Startup: COM launches EXE automatically
- Active: Message pump or wait on event
- Shutdown: Last plugin reference released → EXE exits
Server Application
rdp-plugin-server
Purpose: Windows program that runs inside RDP session and communicates with client plugins via Dynamic Virtual Channels.
Core Features:
- WTS API for DVC channel management
- Session state change notifications
- Render hints for window association
- Two I/O modes: synchronous (WTS API) and asynchronous (file handle)
- Survives disconnect/reconnect cycles
- Graceful shutdown on multiple exit events
Command-Line Options
rdp-plugin-server-cpp.exe [options]
Options:
/filehandle, /f Use asynchronous file-handle I/O instead of WTS API (default: WTS)
/help, /? Show help
Startup Sequence
- Ensure console exists (AttachConsole or AllocConsole)
- Configure console for raw keyboard input
- Create global exit event (manual-reset for shutdown signaling)
- Install console control handler (Ctrl+C/Break)
- Instantiate RdpPluginServer and call Run()
Run Sequence
- CreateMessageWindow() - Hidden window for
WM_WTSSESSION_CHANGE - CheckAndOpenChannel() - Open channel if already in connected session
- MessageLoop() - Wait on exit event + console input + window messages
- After exit:
- Set
m_shouldExit=true StopWorkers()- Cancel I/O, join threadsCloseChannel()- Cleanup DVC, destroy render windowDestroyMessageWindow()- Unregister session notifications
- Set
Session State Management
m_sessionConnected (atomic bool):
- Set to
trueonWTS_REMOTE_CONNECTorWTS_SESSION_LOGON - Set to
falseonWTS_REMOTE_DISCONNECTorWTS_SESSION_LOGOFF
On connect:
- Close any stale channel
- Open new channel (WTSVirtualChannelOpenEx)
- Start worker threads
On disconnect:
- Immediately close channel
- Stop all worker threads
- App continues running, waiting for reconnect (no exit)
Two I/O Modes
WTS API Mode (default):
- Synchronous read/write with timeouts
- Single thread handles both send and receive
- Protected by mutex (WTS APIs not documented as thread-safe)
- Simpler implementation
File Handle Mode (/filehandle):
- Asynchronous OVERLAPPED I/O
- Separate read/write threads
- Manual-reset events for completion notification
- Better performance under high throughput
Exit Methods
- Press Ctrl+C in console
- Press Esc in console or render window
- Click X on render window
Plugin Registration
All client plugins use the same COM CLSID: {D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
InprocServer Registration
HKLM\Software\Classes\CLSID\{D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
(Default) = "RDP Dynamic Virtual Channel Plugin (InprocServer)"
InprocServer32\
(Default) = "C:\path\to\rdp-plugin-inprocserver.dll"
ThreadingModel = "Both"
Register with:
regsvr32 rdp-plugin-inprocserver.dll
LoadLibrary Registration
regsvr32 rdp-plugin-loadlibrary-cpp.dll
This registers the DLL path under:
HKLM\Software\Microsoft\Terminal Server Client\Default\AddIns\rdp-plugin-loadlibrary-cpp.dll
Name = "C:\\path\\to\\rdp-plugin-loadlibrary-cpp.dll"
Note: Unlike traditional COM registration, LoadLibrary registration only stores the DLL path (no CLSID entries). The RDP client loads the DLL and calls the exported VirtualChannelGetInstance function directly.
LocalServer Registration
HKLM\Software\Classes\CLSID\{D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
(Default) = "RDP Dynamic Virtual Channel Plugin (LocalServer)"
LocalServer32\
(Default) = "C:\path\to\rdp-plugin-localserver.exe"
Register with:
# Per-user registration (default)
rdp-plugin-localserver-cpp.exe /register
# Machine-wide registration (requires admin)
rdp-plugin-localserver-cpp.exe /register /machine
# Unregister
rdp-plugin-localserver-cpp.exe /unregister
Running the sample
Build all projects:
.\build.ps1Register a client plugin (choose one):
# InprocServer (recommended) regsvr32 bin\x64\Debug\rdp-plugin-inprocserver.dll # LocalServer (for testing) bin\x64\Debug\rdp-plugin-localserver-cpp.exe /registerRun server inside RDP session:
bin\x64\Debug\rdp-plugin-server-cpp.exeConnect via RDP client to the session:
# Use any supported RDP client: # - mstsc.exe (built-in Windows Remote Desktop Connection) # - Windows App # - Remote Desktop Client ActiveX controlObserve output:
- Server console shows RTT measurements
- Client plugin echoes data back
- Disconnect/reconnect works seamlessly
C++-Specific Considerations
C++/WinRT
C++/WinRT (<winrt/base.h>) is used for modern, ATL-free COM object management throughout the client plugins:
winrt::com_ptr<T>— Smart pointer that callsAddRef/Releaseautomaticallyput_void()/put()— Obtainvoid**/T**forQueryInterfaceand factory output parametersas<T>()— Queried cast; throwswinrt::hresult_no_interfaceon failure
// Acquiring a service interface safely
com_ptr<IWTSPluginServiceProvider> serviceProvider;
channelManager->QueryInterface(IID_IWTSPluginServiceProvider, serviceProvider.put_void());
com_ptr<IUnknown> service;
serviceProvider->GetService(RDCLIENT_BITMAP_RENDER_SERVICE, service.put());
ATL-Free COM Implementation
All COM classes are implemented without ATL or MFC. The ComClassFactory<T> template in rdp-plugin-common provides IClassFactory functionality:
- Manual
IUnknownwithInterlockedIncrement/InterlockedDecrementreference counting - Module lock counting to gate
DllCanUnloadNowand prevent premature DLL unload - No external framework dependencies beyond the Windows SDK and C++/WinRT headers
CRT Linking
The build system supports two CRT modes (controlled by the -ProtobufLink build script parameter):
| Mode | Compiler Flag | When to Use |
|---|---|---|
| Static (default) | /MT |
Self-contained deployment; no CRT DLL dependency |
| Dynamic | /MD |
Shared CRT; smaller binaries when the CRT is already present |
Important: All projects linked into the same process must use the same CRT mode. Mixing
/MTand/MDleads to heap corruption at runtime.
Thread Safety
All implementations use proper synchronization:
- Atomics:
m_shouldExit,m_sessionConnectedfor lock-free flags - Mutexes: Protect WTS API calls, render hint state, sequence counters
- OVERLAPPED I/O: File handle mode uses events for async operations
- COM Threading: InprocServer uses "Both" model for apartment flexibility
What This Demonstrates
Core RDP/DVC Concepts
- Opening dynamic virtual channels in RDP sessions
- Handling fragmented DVC messages (CHANNEL_FLAG_FIRST/LAST)
- Session state change notifications (connect/disconnect/logon/logoff)
- Two I/O patterns: WTS API (sync) vs file handle (async OVERLAPPED)
RDP Graphics APIs
- Render Hints (Server):
WTSSetRenderHint()for content-window association - Bitmap Renderer (Client):
IWTSBitmapRendererfor custom client-side rendering - End-to-end hint ID flow from server to client
- Persistent renderer pool surviving disconnect/reconnect cycles
- Per-renderer threads with 30fps animation
COM Patterns
- Three activation models: InprocServer32, LoadLibrary, LocalServer32
- IUnknown implementation with proper refcounting
- IClassFactory for object creation
- Module lock counting to prevent premature DLL unload
- Cross-apartment marshaling considerations
Message Protocol
- Protocol Buffers for cross-language compatibility
- Length-prefixed framing for stream parsing
- RTT measurement for performance analysis
- Payload echo for bidirectional testing
Next steps
Official Microsoft Documentation
RDP Virtual Channels:
- Terminal Services Virtual Channels: https://learn.microsoft.com/windows/win32/termserv/terminal-services-virtual-channels
- IWTSPlugin Interface: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsplugin
- WTSVirtualChannelOpenEx: https://learn.microsoft.com/windows/win32/api/wtsapi32/nf-wtsapi32-wtsvirtualchannelopenex
Bitmap Renderer API (Client-Side):
- IWTSBitmapRenderService: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderservice
- IWTSBitmapRenderer: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderer
- IWTSBitmapRendererCallback: https://learn.microsoft.com/windows/win32/api/tsvirtualchannels/nn-tsvirtualchannels-iwtsbitmaprenderercallback
Render Hint API (Server-Side):
- WTSSetRenderHint: https://learn.microsoft.com/windows/win32/api/wtsapi32/nf-wtsapi32-wtssetrenderhint
COM Development:
- Component Object Model (COM): https://learn.microsoft.com/windows/win32/com/component-object-model--com--portal
- DllGetClassObject: https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject
- CoRegisterClassObject: https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coregisterclassobject
Session Management:
- WTSRegisterSessionNotificationEx: https://learn.microsoft.com/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotificationex
- WM_WTSSESSION_CHANGE: https://learn.microsoft.com/windows/win32/termserv/wm-wtssession-change
Protocol Buffers:
- Protocol Buffers Documentation: https://protobuf.dev/
License
See repository root for license information.