Share via

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
  • Server output: bin/<Platform>/<Configuration>/rdp-plugin-server-cpp.exe
  • Register:
    • LocalServer: /register (or /register /machine for machine-wide)
    • Inproc: regsvr32 rdp-plugin-inprocserver-cpp.dll
    • LoadLibrary: regsvr32 rdp-plugin-loadlibrary-cpp.dll (exports VirtualChannelGetInstance)
  • Server modes: WTS (sync) or file-handle (async)

Table of Contents

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

  1. Choose and register a client plugin (see Plugin Registration)
  2. Run the server inside an RDP session:
    bin\x64\Debug\rdp-plugin-server-cpp.exe
    
  3. Connect via RDP client to the session
  4. 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
  • 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 DllGetClassObject for COM registration
  • rdp-plugin-loadlibrary - Direct LoadLibrary (DLL)

    • Exports VirtualChannelGetInstance function
    • Alternative to COM activation
    • Loaded directly by RDP client stack
    • Requires registration (registers DLL path in AddIns registry key)
  • 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 DLLs
  • obj\{ProjectName}\{Platform}\{Configuration}\ - Intermediate files

Troubleshooting

If you see "protoc.exe not found":

  1. Delete vcpkg_installed\ folder
  2. Run .\build.ps1 -Clean
  3. 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 counter
  • utc_ticks - Timestamp (100ns ticks since Unix epoch)
  • payload - Data bytes (echo test)
  • render_hint_id - Window/region identifier for RDP optimization
  • render_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 window
  • RENDER_HINT_CLEAR - Removes a previously set hint

Server Workflow:

  1. Create a "render window" to anchor the hint
  2. Calculate hint rectangle (e.g., 80% of client area, centered)
  3. Call WTSSetRenderHint() with RENDER_HINT_MAPPEDWINDOW
  4. Include the returned hintId in ping messages to client
  5. 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:

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:

  1. HandleRenderHint() called with hint ID from server message
  2. Get or create renderer from pool for that mapping ID
  3. Marshal IWTSBitmapRenderer interface to new thread (COM apartment safety)
  4. Thread runs render loop: check pause flag → generate frame → Render() → sleep
  5. 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 IClassFactory implementation for creating COM objects
  • Thread-safe reference counting
  • Module lock management (prevents DLL unload while objects exist)
  • Proper QueryInterface with 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 generation
  • SerializeWithFraming() - Adds length prefix to protobuf bytes
  • TryParseFramed() - 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:

  1. RDP client reads AddIns registry key → finds CLSID {D8B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
  2. RDP client calls CoCreateInstance(CLSID, ...)
  3. COM looks up CLSID in registry → finds InprocServer32 path
  4. COM calls LoadLibrary("rdp-plugin-inprocserver.dll")
  5. COM calls DllGetClassObject() → gets IClassFactory
  6. COM calls factory->CreateInstance() → creates RdpPlugin
  7. 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

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:

  1. COM launches EXE when first client calls CoCreateInstance
  2. EXE registers class factory via CoRegisterClassObject
  3. COM creates plugin instance via factory (cross-process marshaling)
  4. EXE stays running while plugin objects exist
  5. 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

  1. Ensure console exists (AttachConsole or AllocConsole)
  2. Configure console for raw keyboard input
  3. Create global exit event (manual-reset for shutdown signaling)
  4. Install console control handler (Ctrl+C/Break)
  5. Instantiate RdpPluginServer and call Run()

Run Sequence

  1. CreateMessageWindow() - Hidden window for WM_WTSSESSION_CHANGE
  2. CheckAndOpenChannel() - Open channel if already in connected session
  3. MessageLoop() - Wait on exit event + console input + window messages
  4. After exit:
    • Set m_shouldExit=true
    • StopWorkers() - Cancel I/O, join threads
    • CloseChannel() - Cleanup DVC, destroy render window
    • DestroyMessageWindow() - Unregister session notifications

Session State Management

m_sessionConnected (atomic bool):

  • Set to true on WTS_REMOTE_CONNECT or WTS_SESSION_LOGON
  • Set to false on WTS_REMOTE_DISCONNECT or WTS_SESSION_LOGOFF

On connect:

  1. Close any stale channel
  2. Open new channel (WTSVirtualChannelOpenEx)
  3. Start worker threads

On disconnect:

  1. Immediately close channel
  2. Stop all worker threads
  3. 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

  1. Build all projects:

    .\build.ps1
    
  2. Register 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 /register
    
  3. Run server inside RDP session:

    bin\x64\Debug\rdp-plugin-server-cpp.exe
    
  4. Connect 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 control
    
  5. Observe 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 calls AddRef/Release automatically
  • put_void() / put() — Obtain void** / T** for QueryInterface and factory output parameters
  • as<T>() — Queried cast; throws winrt::hresult_no_interface on 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 IUnknown with InterlockedIncrement/InterlockedDecrement reference counting
  • Module lock counting to gate DllCanUnloadNow and 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 /MT and /MD leads to heap corruption at runtime.

Thread Safety

All implementations use proper synchronization:

  • Atomics: m_shouldExit, m_sessionConnected for 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): IWTSBitmapRenderer for 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:

Bitmap Renderer API (Client-Side):

Render Hint API (Server-Side):

COM Development:

Session Management:

Protocol Buffers:

License

See repository root for license information.