Skip to content

Latest commit

 

History

History
127 lines (101 loc) · 4.58 KB

windows-native-window.md

File metadata and controls

127 lines (101 loc) · 4.58 KB

Windows host integration

Native window handle

WinUI 3 desktop features often need the native HWND: global hotkeys, clipboard listeners, SetWindowSubclass, precise SetWindowPos placement, tray icons, and other Win32 ownership APIs all use it.

Generated Rivet windows expose the supported bridge directly:

HWND hwnd = WindowHandle();

From another window type, call the underlying helper after that window has run InitializeComponent:

#include "system_services.hpp"

HWND hwnd = rivet::system::NativeWindowHandle(window);

The helper uses the Windows App SDK IWindowNative::get_WindowHandle ABI and turns a missing interop interface, failed HRESULT, or null handle into a normal C++/WinRT exception. The scaffold owns the required microsoft.ui.xaml.window.h include and Windows system-adapter include path, so product code does not need to recreate projection-specific boilerplate.

Do not find the application's own window by title. FindWindowW is ambiguous when another window has the same title, breaks after localization or title changes, and races window creation. The WinUI object is the authority for its native handle.

Use the handle on the UI thread unless the target Win32 API explicitly permits cross-thread access. Remove listeners, subclasses, and registered hotkeys before the owning window is destroyed; an HWND is not a lifetime-owning reference.

For tray applications, do not reproduce the SetWindowSubclass, TaskbarCreated, TrackPopupMenu, and WM_COMMAND recipe in each host. rivet::system::TrayController owns that HWND-bound lifecycle. The application still owns menu semantics by providing localized TrayMenuItem values and callbacks; see the reference Taskboard's MainWindow.xaml.cpp.

Example:

MainWindow::MainWindow() {
  InitializeComponent();
  auto const hwnd = WindowHandle();
  if (!::AddClipboardFormatListener(hwnd)) {
    winrt::throw_last_error();
  }
}

Non-blocking generated API

The generated C++ client exposes two compatible styles for every RPC and shared State operation. The historical API returns std::future<T>:

rivet_app::API api(*backend);
auto value = api.increment(41).get();

WinUI code should normally use the non-blocking completion companion. It returns the RVT1 request ID immediately, which also permits cancellation with Backend::cancel(id):

auto id = api.increment_async(
    41,
    [dispatcher](rivet_app::Result<std::int64_t> result) {
      try {
        auto value = result.get();
        dispatcher.TryEnqueue([value] {
          // Update WinUI here.
        });
      } catch (std::exception const& error) {
        // Surface the backend or decoding error.
      }
    });

Shared State has the same generated shape, such as get_counter_async(...) and set_counter_async(value, ...).

Polling the backend

Every RPC also gains a bounded-wait companion, name_within(arguments, std::chrono::milliseconds). Its completion resolves a promise, so the call waits up to the timeout and cancels the pending request on a miss; Void RPCs return bool (false = timed out), other results return std::optional<T>. Backend and decoding errors still throw from the final get, matching the blocking call's contract.

// A 400 ms UI tick polls without spawning a thread and without blocking
// past the frame budget:
if (auto value = api.increment_within(41, std::chrono::milliseconds{0})) {
  counter_label().Text(std::to_wstring(*value));
}

This is the sanctioned shape for long-lived polling UIs. Do not spawn a detached std::thread per tick: each one races the reader thread, leaks past teardown, and churns a native thread per frame. Prefer completions when an update push model works; use _within when the UI owns a tick loop, with a zero timeout for a pure poll or a small budget when a bounded wait is acceptable.

Threading

RVT1 responses are decoded on Rivet's native reader thread. Normal completion handlers therefore run there; submission failures or shutdown rejection may complete on the thread that encountered the failure. Dispatch through DispatcherQueue before touching WinUI objects. Rivet isolates exceptions thrown by completion handlers, and Result<T>::get() rethrows backend or decoding errors.

The completion path uses the existing pending-request table, request IDs, cancellation frames, reader thread, and shutdown behavior; it does not create a native thread per RPC. It intentionally does not return IAsyncOperation<T>: projecting Bytes, recursive List, Optional, and Any through a public WinRT ABI would force platform container policy into Rivet's schema.