A lightweight C++ desktop application framework powered by WebView2, inspired by Tauri. Build modern desktop apps with web frontends and C++ backends — single-file deployment, zero source code leakage.
- Single EXE Deployment — WebView2Loader is statically linked, and all frontend assets are embedded into the executable. No external DLLs or files needed.
- Source Code Protection — Frontend resources (HTML/CSS/JS) are compiled into the exe as Windows resources and served from memory via VirtualFS. No temporary files are generated.
- Bidirectional Communication — JS-to-C++ command invocation (Promise-based) and C++-to-JS event emission.
- Window Manipulation API — SetTitle, SetSize, SetPosition, Minimize, Maximize, Restore, SetAlwaysOnTop, SetResizable, SetIcon, and more.
- Window Lifecycle Callbacks — OnClose (with veto capability), OnResize, OnMinimize, OnMaximize, OnFocus.
- File Dialog — Open/Save file dialogs and folder picker via
tauricpp::Dialog. - Clipboard API — Read/write system clipboard via
tauricpp::Clipboard. - DevTools Toggle — Press F12 to open/close DevTools when
config.devtools = true. - SPA Fallback — Non-asset routes automatically serve
/index.htmlfor single-page applications. - Per-Monitor DPI Awareness (Per-Monitor V2) — Manifest-declared Per-Monitor V2 DPI awareness, combined with runtime
SetProcessDpiAwarenessContextfallback andWM_DPICHANGEDhandling. The window and WebView2 are always rendered at physical pixel resolution — no bitmap-stretch blur on 125%/150%/200% scaled displays. - Dark Launch — Window background color matches frontend theme, eliminating white flash on startup.
- Dev/Prod Dual Mode — Automatically loads from filesystem in development, from embedded resources in production. No code changes required.
- Minimal Dependencies — Only WebView2 SDK and nlohmann/json. No Chromium bundled, no .NET runtime, no Qt.
- Persistent Frontend Storage — IndexedDB, LocalStorage, and SessionStorage work reliably across app launches. WebView2 user data is stored in a stable temp directory (
%TEMP%/tauricpp_<exe_name>/) instead of per-PID folders, ensuring frontend-side persistence survives restarts. - C++17 — Modern C++ with clean API design.
- Visual Studio 2019+ (with C++ Desktop Development workload)
- vcpkg (classic mode, installed at
C:\vcpkg) - Python 3.7+ (for resource packing script)
- PowerShell 5.1+
.\build.ps1 -SetupDepsThis will:
- Download
nlohmann/json.hpptothird_party/ - Install
webview2via vcpkg (C:\vcpkg install webview2:x64-windows)
.\build.ps1 # Build (Release)
.\build.ps1 -Clean # Clean build directory and rebuildOutput: build\sample.exe
.\build\sample.exeTauriCPP/
├── CMakeLists.txt # CMake build configuration
├── build.ps1 # PowerShell build script (Ninja + MSVC)
├── vcpkg.json # vcpkg dependency manifest
├── include/tauricpp/
│ ├── app.hpp # Application entry point
│ ├── bridge.hpp # Frontend-backend communication bridge
│ ├── window.hpp # Win32 + WebView2 window
│ ├── virtual_fs.hpp # In-memory virtual filesystem
│ ├── embedded_dll.hpp # Embedded DLL loader (general purpose)
│ ├── dialog.hpp # File dialog API (open/save/pick folder)
│ └── clipboard.hpp # Clipboard API (read/write text)
├── src/
│ ├── app.cpp
│ ├── bridge.cpp # Contains injected JS bridge code
│ ├── window.cpp # WebView2 initialization & resource interception
│ ├── virtual_fs.cpp
│ ├── embedded_dll.cpp # Embedded DLL loader implementation
│ ├── dialog.cpp # IFileDialog implementation
│ └── clipboard.cpp # Win32 clipboard implementation
├── sample/
│ ├── src/main.cpp # Example application
│ └── frontend/
│ ├── index.html
│ ├── css/style.css
│ └── js/app.js
├── tools/
│ └── pack_resources.py # Frontend -> .rc resource packing tool
└── third_party/
└── nlohmann/json.hpp # JSON library (single header)
#include "tauricpp/app.hpp"
#include "tauricpp/dialog.hpp"
#include "tauricpp/clipboard.hpp"
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
// WebView2Loader is statically linked — no DLL loading needed
// Configure application
tauricpp::App::Config config;
config.window_config.title = "My App";
config.window_config.width = 1200;
config.window_config.height = 800;
config.window_config.center = true;
config.window_config.devtools = true; // Enable F12 DevTools toggle
config.window_config.bg_color = RGB(15, 12, 41); // Dark background (no white flash)
tauricpp::App app(config);
// Register commands callable from frontend
app.GetBridge().RegisterCommand("greet", [](const nlohmann::json& args) {
std::string name = args.value("name", "World");
return nlohmann::json{{"message", "Hello, " + name + "!"}};
});
// Emit events to frontend
app.GetBridge().Emit("timer", nlohmann::json{{"tick", 1}});
return app.Run();
}// JS -> C++: Invoke a command (returns Promise)
const result = await __tauricpp__.invoke('greet', { name: 'World' });
// result: { "message": "Hello, World!" }
// C++ -> JS: Listen for events (returns unsubscribe function)
const unlisten = __tauricpp__.listen('timer', function(data) {
console.log('Timer tick:', data);
});
// Remove listener
__tauricpp__.removeListener('timer', callback);
// Or use returned unsubscribe function
unlisten();Frontend files in sample/frontend/ are automatically packed into the exe during build via tools/pack_resources.py. At runtime, VirtualFS loads them from the exe resource section and serves them through WebResourceRequested interception — no temporary files are written to disk.
┌─────────────────────────────────────────────────┐
│ sample.exe │
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ App │ │ Bridge │ │ Window │ │
│ │ │──│ │──│ │ │
│ │ Config │ │ Register │ │ Win32 + │ │
│ │ Run() │ │ Emit() │ │ WebView2 │ │
│ └──────────┘ └───────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────────────┐ │
│ │ VirtualFS │ │
│ │ │ │
│ │ RegisterFile │ │
│ │ FindFile │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────┐ │
│ │ Windows Resource Section │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Frontend Assets (HTML/CSS/JS/...) │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌───────────┐ │
│ │ Dialog │ │ Clipboard │ │
│ │ OpenFile │ │ ReadText │ │
│ │ SaveFile │ │ WriteText │ │
│ └──────────┘ └───────────┘ │
└─────────────────────────────────────────────────┘
-
Build Time:
pack_resources.pyscans the frontend directory, generates a.rcresource script with numeric IDs, and compiles it into the exe. WebView2Loader is statically linked viaWebView2LoaderStatic.lib. -
Runtime - Resource Serving:
SetVirtualHostNameToFolderMappingmapstauricpp.appto a directory.WebResourceRequestedintercepts all requests tohttps://tauricpp.app/*and serves content fromVirtualFS(in-memory), with proper MIME types, CORS, and cache headers. SPA fallback serves/index.htmlfor non-asset routes. -
Runtime - Communication:
AddScriptToExecuteOnDocumentCreatedinjects the bridge JS before any page script runs.postMessage/WebMessageReceivedhandles JS→C++, whileExecuteScripthandles C++→JS. -
Runtime - Dark Launch: The window background color (
Config::bg_color) and WebView2 default background color are both set to match the frontend theme, eliminating the white flash during startup.
| TauriCPP | Electron | Tauri | WPF | Core UI | |
|---|---|---|---|---|---|
| Language | C++ | JS/TS | Rust | C#/.NET | C/JS(QuickJS) |
| Rendering | WebView2 (Edge) | Chromium | WebView2/WebKitGTK | DirectX (.NET) | Direct2D/D3D11 |
| Frontend | Any web framework | Any web framework | Any web framework | XAML | .uix (Vue SFC-like) |
| Binary Size | ~1-10 MB | 100+ MB | 3-10 MB | Requires .NET runtime | ~3 MB DLL |
| Memory Usage | 50-80 MB | 150+ MB | 30-80 MB | 80+ MB | < 30 MB |
| Startup | < 500ms | 1-3s | < 500ms | 0.5-1s | < 200ms |
| Source Protection | Yes (embedded in exe, served from memory) | No (asar is archive, not encryption) | Limited | No (IL can be decompiled) | Yes (compiled binary) |
| Single File Deploy | Yes | No | Yes (bundled) | No | Yes (single DLL) |
| Cross-Platform | Windows only | Windows/macOS/Linux | Windows/macOS/Linux | Windows only | Windows only |
| Runtime Dependency | WebView2 (pre-installed on Win10/11) | Bundled Chromium | WebView2/WebKitGTK | .NET Runtime | None |
| Backend Ecosystem | C++ native libraries | npm ecosystem | Rust crates ecosystem | NuGet ecosystem | C ABI, any language |
Electron bundles the entire Chromium engine, resulting in 100+ MB binaries and 150+ MB memory usage. TauriCPP uses the system's WebView2 (pre-installed on Windows 10/11), keeping the binary under 10 MB. Frontend source code is embedded in the exe and served from memory — no asar archives that can be extracted.
Tauri is the most similar project, but it requires Rust. TauriCPP uses C++, giving direct access to the Windows API and existing C++ codebases without FFI overhead. Both use WebView2 on Windows and achieve similar binary sizes. Tauri offers cross-platform support (macOS/Linux via WebKitGTK), while TauriCPP is Windows-only but with a simpler build toolchain.
WPF requires the .NET runtime and uses XAML for UI definition. TauriCPP uses standard web technologies (HTML/CSS/JS), leveraging the massive web ecosystem and allowing teams to reuse frontend skills. WPF applications can be decompiled from IL, while TauriCPP embeds frontend resources in the native binary.
Core UI is a native rendering framework using Direct2D with a custom .uix component format and QuickJS engine — extremely lightweight (3 MB, < 30 MB memory). However, it uses a custom UI framework with limited CSS support and a small widget set. TauriCPP uses the full web platform (any CSS, any JS framework, full DOM), giving access to the entire npm ecosystem. The tradeoff is higher memory usage (WebView2 overhead) but vastly greater UI flexibility and developer familiarity.
tauricpp::App::Config config;
config.window_config.title = "My App";
config.window_config.width = 1024;
config.window_config.height = 768;
config.window_config.center = true;
config.window_config.resizable = true;
config.window_config.always_on_top = false;
config.window_config.devtools = true;
config.window_config.bg_color = RGB(15, 12, 41); // Dark background, no white flash
config.window_config.start_url = "https://tauricpp.app/index.html";
config.dev_mode = false;
tauricpp::App app(config);
app.OnSetup([](tauricpp::App& app) { /* called before message loop */ });
int exitCode = app.Run();auto& bridge = app.GetBridge();
// Register command (JS callable via __tauricpp__.invoke('cmd', args))
bridge.RegisterCommand("cmd_name", [](const nlohmann::json& args) -> nlohmann::json {
return {{"result", "ok"}};
});
// Type-safe command registration (Args/Result must be JSON-serializable)
bridge.RegisterCommand<MyArgs, MyResult>("typed_cmd", [](const MyArgs& args) -> MyResult {
return MyResult{...};
});
// Unregister a command
bridge.UnregisterCommand("cmd_name");
// Emit event to frontend (JS listens via __tauricpp__.listen('event', cb))
bridge.Emit("event_name", nlohmann::json{{"key", "value"}});auto& window = app.GetWindow();
// Window manipulation
window.SetTitle("New Title");
window.SetSize(800, 600);
window.SetPosition(100, 100);
window.SetAlwaysOnTop(true);
window.SetResizable(false);
window.SetIcon(LoadIcon(nullptr, IDI_APPLICATION));
window.Minimize();
window.Maximize();
window.Restore();
// Query state
bool min = window.IsMinimized();
bool max = window.IsMaximized();
bool focus = window.IsFocused();
// Lifecycle callbacks
window.OnClose([]() -> bool { return true; }); // return false to veto close
window.OnResize([](int w, int h) { /* handle resize */ });
window.OnMinimize([]() { /* handle minimize */ });
window.OnMaximize([]() { /* handle maximize */ });
window.OnFocus([]() { /* handle focus */ });
// DevTools
window.ToggleDevTools();
// Execute JS
window.ExecuteJs("console.log('Hello from C++')");
HWND hwnd = window.GetHwnd();// Open file dialog
tauricpp::Dialog::OpenOptions opts;
opts.title = "Open File";
opts.filters = {{"Text Files", "*.txt"}, {"All Files", "*.*"}};
opts.multi_select = true;
auto files = tauricpp::Dialog::OpenFile(hwnd, opts);
// Save file dialog
tauricpp::Dialog::SaveOptions saveOpts;
saveOpts.title = "Save File";
saveOpts.default_filename = "untitled.txt";
auto path = tauricpp::Dialog::SaveFile(hwnd, saveOpts);
// Folder picker (with configurable properties)
tauricpp::Dialog::PickFolderOptions folderOpts;
folderOpts.title = "Select Folder";
folderOpts.default_path = "C:\\Users"; // optional default folder
auto folder = tauricpp::Dialog::PickFolder(hwnd, folderOpts);
// Or use the simple overload
auto folder2 = tauricpp::Dialog::PickFolder(hwnd, "Select Folder");
// Message boxes
tauricpp::Dialog::ShowInfo(hwnd, "Info", "Operation completed");
bool confirmed = tauricpp::Dialog::AskConfirm(hwnd, "Confirm", "Are you sure?");// Read clipboard
auto text = tauricpp::Clipboard::ReadText();
if (text) { std::string value = *text; }
// Write clipboard
tauricpp::Clipboard::WriteText("Hello, Clipboard!");
// Clear clipboard
tauricpp::Clipboard::Clear();auto& vfs = app.GetFS();
// Register a file in the virtual filesystem
vfs.RegisterFile("/path/to/file.html", "<h1>Hello</h1>", "text/html");
// Register with move semantics (avoids copy for large files)
std::vector<uint8_t> data = LoadLargeFile();
vfs.RegisterFile("/path/to/large.bin", std::move(data), "application/octet-stream");
// Find a file (pointer version, no copy)
const tauricpp::VirtualFS::VFile* file = vfs.FindFile("/path/to/file.html");
if (file) { /* file->data, file->mime_type */ }
// Find a file (copy version)
tauricpp::VirtualFS::VFile fileCopy;
if (vfs.FindFile("/path/to/file.html", fileCopy)) { /* ... */ }.\build.ps1 # Build (Release, Ninja + MSVC)
.\build.ps1 -Clean # Clean and rebuild
.\build.ps1 -SetupDeps # Install dependencies (first time only)- Windows 10/11 (WebView2 runtime, pre-installed on most systems)
- Visual Studio 2019+ with C++ Desktop Development workload
- CMake 3.20+
- Ninja (comes with VS C++ workload)
- vcpkg (classic mode)
- Python 3.7+
- New: Folder picker now accepts configurable properties via
tauricpp::Dialog::PickFolderOptions(title,default_path); the originalPickFolder(hwnd, title)overload is retained for compatibility. - New: Sample File Dialog adds a "Select Folder" button that writes the chosen folder path into a text box, along with
titleanddefault_pathinput fields. - New: WebView2 Runtime availability check at startup. If the runtime is missing, a friendly message box is shown with install instructions; Windows 7 users are directed to the last supported fixed version (109.0.1518.140).
- New: Persistent frontend storage — IndexedDB, LocalStorage, and SessionStorage now survive app restarts
- Fix: WebView2 user data folder changed from
%TEMP%\tauricpp_webview2_<PID>to%TEMP%\tauricpp_<exe_name>/. Previously the per-process-ID temp folder was recreated on every launch, causing all browser-side storage (IndexedDB, cache, etc.) to be lost
- Fix: Rendering on high-DPI (125% / 150% / 200%) displays was blurry compared to the Edge browser. The application now declares Per-Monitor V2 DPI awareness via the embedded manifest (fallback
SetProcessDpiAwarenessContextat runtime), scales window geometry using physical pixels viaMulDiv+ DPI-awareAdjustWindowRectExForDpi, and handlesWM_DPICHANGEDto relayoutWebView2::put_Boundswhen the window is dragged across monitors with different scaling.
- New: WebView2Loader statically linked — no more DLL embedding, delay-load hooks, or runtime DLL extraction
- New: Window manipulation API (SetTitle, SetSize, SetPosition, Minimize, Maximize, Restore, SetAlwaysOnTop, SetResizable, SetIcon)
- New: Window lifecycle callbacks (OnClose with veto, OnResize, OnMinimize, OnMaximize, OnFocus)
- New: File Dialog API (
tauricpp::Dialog— OpenFile, SaveFile, PickFolder, ShowInfo, AskConfirm) - New: Clipboard API (
tauricpp::Clipboard— ReadText, WriteText, Clear) - New: DevTools toggle (F12 shortcut,
ToggleDevTools(),Config::devtools) - New: SPA fallback (non-asset routes automatically serve
/index.html) - New: Type-safe command registration (
RegisterCommand<ArgsT, ResultT>) - New:
UnregisterCommandfor dynamic command management - New:
listen()returns unsubscribe function in frontend API - New: Dark launch — window background color matches frontend theme, eliminating white flash
- New: Config options
always_on_top,devtools,bg_color,dev_mode - Fix: Bridge deadlock — handler now called outside mutex lock
- Fix: Bridge race condition —
execute_js_copied to local before use - Fix: XSS vulnerability in
Emit— event names now JSON-encoded instead of string-concatenated - Fix: Use-after-free in detached threads —
atomic<bool>shutdown signal +OnClosecallback - Fix: Non-ASCII path handling — all
wstring(string.begin(), string.end())replaced with properMultiByteToWideChar - Fix:
FindFileunnecessary copy — added pointer version and move-semanticsRegisterFile - Fix: Bridge JS error swallowing —
catch(ex) {}replaced withconsole.error - Fix: CMake resource tracking — frontend file changes now trigger repack
- Fix: Delete copy/move constructors on non-copyable classes
If you find TauriCPP helpful, consider buying me a coffee!
MIT
