Repository: tauri-apps/tauri (language: rust)

Issue:
[bug] `GetDpiForWindow could not be located` on Windows <1607
### Describe the bug

Hi.

After the latest 2.3.0 update my app doesn't seem to work on old windows versions.
I suppose this is related to PR #12817 ([this](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-runtime-wry/src/undecorated_resizing.rs#L85) and [this](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-runtime-wry/src/undecorated_resizing.rs#L305) lines). But Tao assumes that function may not exist sometimes ([here](https://github.com/tauri-apps/tao/blob/dev/src/platform_impl/windows/dpi.rs#L76))

<img width="719" alt="Image" src="https://github.com/user-attachments/assets/196d5707-2789-4e07-95d3-01a246dfe7da" />

### Reproduction

_No response_

### Expected behavior

_No response_

### Full `tauri info` output

```text
Not really related, but:
[✔] Environment
    - OS: Mac OS 13.5.2 arm64 (X64)
    ✔ Xcode Command Line Tools: installed
    ✔ rustc: 1.85.0 (4d91de4e4 2025-02-17)
    ✔ cargo: 1.85.0 (d73d2caf9 2024-12-31)
    ✔ rustup: 1.27.1 (2024-04-24)
    ✔ Rust toolchain: stable-aarch64-apple-darwin (environment override by RUSTUP_TOOLCHAIN)
    - node: 23.4.0
    - pnpm: 9.13.2
    - yarn: 1.22.22
    - npm: 10.9.2
    - bun: 1.2.2

[-] Packages
    - tauri 🦀: 2.3.0
    - tauri-build 🦀: 2.0.6
    - wry 🦀: 0.50.1
    - tao 🦀: 0.32.4
    - tauri-cli 🦀: 2.2.7
    - @tauri-apps/api : 2.1.1 (outdated, latest: 2.3.0)
    - @tauri-apps/cli : 2.2.7 (outdated, latest: 2.3.0)

[-] Plugins
    - tauri-plugin-shell 🦀: 2.2.0
    - @tauri-apps/plugin-shell : not installed!
    - tauri-plugin-dialog 🦀: 2.2.0
    - @tauri-apps/plugin-dialog : 2.0.1 (outdated, latest: 2.2.0)
    - tauri-plugin-deep-link 🦀: 2.2.0
    - @tauri-apps/plugin-deep-link : not installed!
    - tauri-plugin-log 🦀: 2.2.3
    - @tauri-apps/plugin-log : 2.0.0 (outdated, latest: 2.2.3)
    - tauri-plugin-updater 🦀: 2.5.1
    - @tauri-apps/plugin-updater : not installed!
    - tauri-plugin-fs 🦀: 2.2.0
    - @tauri-apps/plugin-fs : not installed!
    - tauri-plugin-window-state 🦀: 2.2.1
    - @tauri-apps/plugin-window-state : not installed!

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../dist
    - devUrl: http://localhost:5173/
    - bundler: Vite
```

### Stack trace

```text

```

### Additional context

_No response_


Discussion / hints:

Fix patch:
diff --git a/.changes/windows-crash.md b/.changes/windows-crash.md
new file mode 100644
index 000000000000..3b138f9d1341
--- /dev/null
+++ b/.changes/windows-crash.md
@@ -0,0 +1,6 @@
+---
+"tauri": "patch:bug"
+"tauri-runtime-wry": "patch:bug"
+---
+
+Fix crash on Windows because of missing functions on older Windows systems, regression in 2.3.0
diff --git a/Cargo.lock b/Cargo.lock
index 46396d09ed12..2f400033223d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8797,6 +8797,7 @@ dependencies = [
  "objc2 0.6.0",
  "objc2-app-kit",
  "objc2-foundation 0.3.0",
+ "once_cell",
  "percent-encoding",
  "raw-window-handle",
  "softbuffer",
diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml
index 2c5a9ba1a93f..efdae8dd8ee6 100644
--- a/crates/tauri-runtime-wry/Cargo.toml
+++ b/crates/tauri-runtime-wry/Cargo.toml
@@ -35,6 +35,7 @@ log = "0.4.21"
 [target."cfg(windows)".dependencies]
 webview2-com = "0.36"
 softbuffer = { version = "0.4", default-features = false }
+once_cell = "1.20"
 
 [target."cfg(windows)".dependencies.windows]
 version = "0.60"
diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs
index a865c031057c..07626250bf50 100644
--- a/crates/tauri-runtime-wry/src/lib.rs
+++ b/crates/tauri-runtime-wry/src/lib.rs
@@ -134,6 +134,7 @@ type IpcHandler = dyn Fn(Request<String>) + 'static;
   target_os = "openbsd"
 ))]
 mod undecorated_resizing;
+mod util;
 mod webview;
 mod window;
 
diff --git a/crates/tauri-runtime-wry/src/undecorated_resizing.rs b/crates/tauri-runtime-wry/src/undecorated_resizing.rs
index 82fe127d429d..62a1d1439374 100644
--- a/crates/tauri-runtime-wry/src/undecorated_resizing.rs
+++ b/crates/tauri-runtime-wry/src/undecorated_resizing.rs
@@ -78,11 +78,12 @@ fn hit_test(
 
 #[cfg(windows)]
 mod windows {
+  use crate::util;
+
   use super::{hit_test, HitTestResult};
 
   use windows::core::*;
   use windows::Win32::System::LibraryLoader::*;
-  use windows::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
   use windows::Win32::UI::WindowsAndMessaging::*;
   use windows::Win32::{Foundation::*, UI::Shell::SetWindowSubclass};
   use windows::Win32::{Graphics::Gdi::*, UI::Shell::DefSubclassProc};
@@ -302,9 +303,9 @@ mod windows {
 
         let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32);
 
-        let dpi = unsafe { GetDpiForWindow(child) };
-        let border_x = GetSystemMetricsForDpi(SM_CXFRAME, dpi);
-        let border_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi);
+        let dpi = unsafe { util::hwnd_dpi(child) };
+        let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi);
+        let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi);
 
         let res = hit_test(
           rect.left,
@@ -348,9 +349,9 @@ mod windows {
             return DefWindowProcW(child, msg, wparam, lparam);
           }
 
-          let padded_border = GetSystemMetrics(SM_CXPADDEDBORDER);
-          let border_x = GetSystemMetrics(SM_CXFRAME) + padded_border;
-          let border_y = GetSystemMetrics(SM_CYFRAME) + padded_border;
+          let dpi = unsafe { util::hwnd_dpi(child) };
+          let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi);
+          let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi);
 
           hit_test(
             rect.left,
@@ -415,9 +416,9 @@ mod windows {
     // and so we need create a cut out in the middle for the parent and other child
     // windows like the webview can receive mouse events.
 
-    let dpi = unsafe { GetDpiForWindow(hwnd) };
-    let border_x = GetSystemMetricsForDpi(SM_CXFRAME, dpi);
-    let border_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi);
+    let dpi = unsafe { util::hwnd_dpi(hwnd) };
+    let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi);
+    let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi);
 
     let hrgn1 = CreateRectRgn(0, 0, width, height);
 
diff --git a/crates/tauri-runtime-wry/src/util.rs b/crates/tauri-runtime-wry/src/util.rs
new file mode 100644
index 000000000000..8270ec3c3839
--- /dev/null
+++ b/crates/tauri-runtime-wry/src/util.rs
@@ -0,0 +1,118 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#[cfg_attr(not(windows), allow(unused_imports))]
+pub use imp::*;
+
+#[cfg(not(windows))]
+mod imp {}
+
+#[cfg(windows)]
+mod imp {
+  use std::{iter::once, os::windows::ffi::OsStrExt};
+
+  use once_cell::sync::Lazy;
+  use windows::{
+    core::{HRESULT, PCSTR, PCWSTR},
+    Win32::{
+      Foundation::*,
+      Graphics::Gdi::*,
+      System::LibraryLoader::{GetProcAddress, LoadLibraryW},
+      UI::{HiDpi::*, WindowsAndMessaging::*},
+    },
+  };
+
+  pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    string.as_ref().encode_wide().chain(once(0)).collect()
+  }
+
+  // Helper function to dynamically load function pointer.
+  // `library` and `function` must be zero-terminated.
+  pub(super) fn get_function_impl(library: &str, function: &str) -> FARPROC {
+    let library = encode_wide(library);
+    assert_eq!(function.chars().last(), Some('\0'));
+
+    // Library names we will use are ASCII so we can use the A version to avoid string conversion.
+    let module = unsafe { LoadLibraryW(PCWSTR::from_raw(library.as_ptr())) }.unwrap_or_default();
+    if module.is_invalid() {
+      return None;
+    }
+
+    unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
+  }
+
+  macro_rules! get_function {
+    ($lib:expr, $func:ident) => {
+      $crate::util::get_function_impl($lib, concat!(stringify!($func), '\0'))
+        .map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
+    };
+  }
+
+  type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
+  type GetDpiForMonitor = unsafe extern "system" fn(
+    hmonitor: HMONITOR,
+    dpi_type: MONITOR_DPI_TYPE,
+    dpi_x: *mut u32,
+    dpi_y: *mut u32,
+  ) -> HRESULT;
+  type GetSystemMetricsForDpi =
+    unsafe extern "system" fn(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32;
+
+  static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
+    Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
+  static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
+    Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor));
+  static GET_SYSTEM_METRICS_FOR_DPI: Lazy<Option<GetSystemMetricsForDpi>> =
+    Lazy::new(|| get_function!("user32.dll", GetSystemMetricsForDpi));
+
+  #[allow(non_snake_case)]
+  pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
+    let hdc = GetDC(Some(hwnd));
+    if hdc.is_invalid() {
+      return USER_DEFAULT_SCREEN_DPI;
+    }
+    if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
+      // We are on Windows 10 Anniversary Update (1607) or later.
+      match GetDpiForWindow(hwnd) {
+        0 => USER_DEFAULT_SCREEN_DPI, // 0 is returned if hwnd is invalid
+        dpi => dpi,
+      }
+    } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
+      // We are on Windows 8.1 or later.
+      let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
+      if monitor.is_invalid() {
+        return USER_DEFAULT_SCREEN_DPI;
+      }
+
+      let mut dpi_x = 0;
+      let mut dpi_y = 0;
+      if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() {
+        dpi_x
+      } else {
+        USER_DEFAULT_SCREEN_DPI
+      }
+    } else {
+      // We are on Vista or later.
+      if IsProcessDPIAware().as_bool() {
+        // If the process is DPI aware, then scaling must be handled by the application using
+        // this DPI value.
+        GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32
+      } else {
+        // If the process is DPI unaware, then scaling is performed by the OS; we thus return
+        // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
+        // application and the WM.
+        USER_DEFAULT_SCREEN_DPI
+      }
+    }
+  }
+
+  #[allow(non_snake_case)]
+  pub unsafe fn get_system_metrics_for_dpi(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32 {
+    if let Some(GetSystemMetricsForDpi) = *GET_SYSTEM_METRICS_FOR_DPI {
+      GetSystemMetricsForDpi(nindex, dpi)
+    } else {
+      GetSystemMetrics(nindex)
+    }
+  }
+}


Test patch: