Repository: electron/electron (language: cpp)

Issue:
Regression: `win.getNativeWindowHandle()` is broken on macOS
### Confirmation

- [x] I am a [maintainer](https://github.com/orgs/electron/people) of the Electron project. (If not, please create a [different issue type](https://github.com/electron/electron/issues/new/).)

### Description

## Summary

For [about a month now](https://chromium-review.googlesource.com/c/chromium/src/+/6361987), `getNativeWindowHandle()` is no longer returning a `NSView*` on macOS.

I found this bug today when [this new buffer safety PR](https://github.com/electron/electron/pull/46724) surfaced it. So I guess that PR is doing its job! 😆

We also have a [spec](https://github.com/electron/electron/blob/dd03cceda0384f62540b0cea650e73b091eab34b/spec/api-browser-window-spec.ts#L6281) that should detect this, but it seems to be [disabled in CI](https://github.com/electron/electron/blob/dd03cceda0384f62540b0cea650e73b091eab34b/.github/workflows/pipeline-segment-electron-test.yml#L195).

## Storytime

`BaseWindow::GetNativeWindowHandle()` works by doing a bytewise copy of NativeWindowHandle, which on macOS is a typedef for the Chromium type `gfx::NativeView`.

In the very early days of Electron, `gfx::NativeView` was, in fact, a `NSView*` and so the code worked as expected.

This changed in October 2018 with the [upstream change](https://chromium-review.googlesource.com/c/1270343) "Make gfx::NativeView and gfx::NativeWindow not be NSView and NSWindow".  Happily, the new type was a wrapper class whose only field was the `NSView` pointer, so a bytewise copy worked anyway. The code still worked as expected... by accident. 

The type changed again [in March 2025](https://chromium-review.googlesource.com/c/chromium/src/+/6361987). It's now a subclass of [base::apple::WeakNSView](https://chromium.googlesource.com/chromium/src/+/main/base/apple/owned_objc.h#59) which [holds](https://chromium.googlesource.com/chromium/src/+/main/base/apple/owned_objc.h#78) a `std::unique_ptr`. If that was a pointer to a NSView, in theory a bytewise copy would _still_ work as before due to the same lucky accident. Unfortunately, it's a pointer to [an opaque data type](https://chromium.googlesource.com/chromium/src/+/main/base/apple/owned_objc.mm#13) that in turn [holds](https://chromium.googlesource.com/chromium/src/+/main/base/apple/owned_objc.mm#14) a NSView*. So instead of returning a `NSView*`, the bytewise copy is now returning a `NSView**`. 😵

**BUT!** To add insult to injury, it's a little worse than that. Since the new wrapper class [also has another field](https://chromium-review.googlesource.com/c/chromium/src/+/6361987/10/ui/gfx/native_widget_types.h), we're also writing another `sizeof(uintptr_t)` bytes at the end of that `NSView**`. 



Discussion / hints:

Fix patch:
diff --git a/filenames.gni b/filenames.gni
index ea1637c990309..45fafbdb52eb5 100644
--- a/filenames.gni
+++ b/filenames.gni
@@ -125,6 +125,7 @@ filenames = {
     "shell/browser/animation_util.h",
     "shell/browser/animation_util_mac.mm",
     "shell/browser/api/electron_api_app_mac.mm",
+    "shell/browser/api/electron_api_base_window_mac.mm",
     "shell/browser/api/electron_api_menu_mac.h",
     "shell/browser/api/electron_api_menu_mac.mm",
     "shell/browser/api/electron_api_native_theme_mac.mm",
diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc
index 848cc6cc2c9f8..a7983dd39c8bc 100644
--- a/shell/browser/api/electron_api_base_window.cc
+++ b/shell/browser/api/electron_api_base_window.cc
@@ -70,6 +70,7 @@ namespace electron::api {
 
 namespace {
 
+#if !BUILDFLAG(IS_MAC)
 // Converts binary data to Buffer.
 v8::Local<v8::Value> ToBuffer(v8::Isolate* isolate, void* val, int size) {
   auto buffer = node::Buffer::Copy(isolate, static_cast<char*>(val), size);
@@ -78,6 +79,7 @@ v8::Local<v8::Value> ToBuffer(v8::Isolate* isolate, void* val, int size) {
   else
     return buffer.ToLocalChecked();
 }
+#endif
 
 [[nodiscard]] constexpr std::array<int, 2U> ToArray(const gfx::Size size) {
   return {size.width(), size.height()};
@@ -778,6 +780,7 @@ std::string BaseWindow::GetMediaSourceId() const {
   return window_->GetDesktopMediaID().ToString();
 }
 
+#if !BUILDFLAG(IS_MAC)
 v8::Local<v8::Value> BaseWindow::GetNativeWindowHandle() {
   // TODO(MarshallOfSound): Replace once
   // https://chromium-review.googlesource.com/c/chromium/src/+/1253094/ has
@@ -785,6 +788,7 @@ v8::Local<v8::Value> BaseWindow::GetNativeWindowHandle() {
   NativeWindowHandle handle = window_->GetNativeWindowHandle();
   return ToBuffer(isolate(), &handle, sizeof(handle));
 }
+#endif
 
 void BaseWindow::SetProgressBar(double progress, gin_helper::Arguments* args) {
   gin_helper::Dictionary options;
diff --git a/shell/browser/api/electron_api_base_window_mac.mm b/shell/browser/api/electron_api_base_window_mac.mm
new file mode 100644
index 0000000000000..148ec216283f7
--- /dev/null
+++ b/shell/browser/api/electron_api_base_window_mac.mm
@@ -0,0 +1,32 @@
+// Copyright (c) 2025 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/api/electron_api_base_window.h"
+
+#include "electron/buildflags/buildflags.h"
+#include "shell/browser/api/electron_api_view.h"
+#include "shell/browser/native_window.h"
+#include "shell/common/node_includes.h"
+
+namespace {
+
+// Converts binary data to Buffer.
+v8::Local<v8::Value> ToBuffer(v8::Isolate* isolate, void* val, int size) {
+  auto buffer = node::Buffer::Copy(isolate, static_cast<char*>(val), size);
+  if (buffer.IsEmpty())
+    return v8::Null(isolate);
+  else
+    return buffer.ToLocalChecked();
+}
+
+}  // namespace
+
+namespace electron::api {
+
+v8::Local<v8::Value> BaseWindow::GetNativeWindowHandle() {
+  NSView* handle = window_->GetNativeWindowHandle().GetNativeNSView();
+  return ToBuffer(isolate(), &handle, sizeof(handle));
+}
+
+}  // namespace electron::api
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index 0779c4919b29a..bf5f6ec4b44b2 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -3743,15 +3743,17 @@ void WebContents::SetDevToolsWebContents(const WebContents* devtools) {
     inspectable_web_contents_->SetDevToolsWebContents(devtools->web_contents());
 }
 
+#if !BUILDFLAG(IS_MAC)
 v8::Local<v8::Value> WebContents::GetNativeView(v8::Isolate* isolate) const {
   gfx::NativeView ptr = web_contents()->GetNativeView();
-  auto buffer = node::Buffer::Copy(isolate, reinterpret_cast<char*>(&ptr),
-                                   sizeof(gfx::NativeView));
+  auto buffer =
+      node::Buffer::Copy(isolate, reinterpret_cast<char*>(&ptr), sizeof(ptr));
   if (buffer.IsEmpty())
     return v8::Null(isolate);
   else
     return buffer.ToLocalChecked();
 }
+#endif
 
 v8::Local<v8::Value> WebContents::DevToolsWebContents(v8::Isolate* isolate) {
   if (devtools_web_contents_.IsEmpty())
diff --git a/shell/browser/api/electron_api_web_contents_mac.mm b/shell/browser/api/electron_api_web_contents_mac.mm
index 52c4362839610..1358b3dde95da 100644
--- a/shell/browser/api/electron_api_web_contents_mac.mm
+++ b/shell/browser/api/electron_api_web_contents_mac.mm
@@ -6,6 +6,7 @@
 #include "shell/browser/api/electron_api_web_contents.h"
 #include "shell/browser/ui/cocoa/event_dispatching_window.h"
 #include "shell/browser/web_contents_preferences.h"
+#include "shell/common/node_includes.h"
 #include "ui/base/cocoa/command_dispatcher.h"
 #include "ui/base/cocoa/nsmenu_additions.h"
 #include "ui/base/cocoa/nsmenuitem_additions.h"
@@ -92,4 +93,22 @@ - (void)redispatchKeyEvent:(NSEvent*)event;
   return false;
 }
 
+namespace {
+
+// Converts binary data to Buffer.
+v8::Local<v8::Value> ToBuffer(v8::Isolate* isolate, void* val, int size) {
+  auto buffer = node::Buffer::Copy(isolate, static_cast<char*>(val), size);
+  if (buffer.IsEmpty())
+    return v8::Null(isolate);
+  else
+    return buffer.ToLocalChecked();
+}
+
+}  // namespace
+
+v8::Local<v8::Value> WebContents::GetNativeView(v8::Isolate* isolate) const {
+  NSView* handle = web_contents()->GetNativeView().GetNativeNSView();
+  return ToBuffer(isolate, &handle, sizeof(handle));
+}
+
 }  // namespace electron::api
diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h
index cc4d150ca72b1..831cf8947aa85 100644
--- a/shell/browser/native_window.h
+++ b/shell/browser/native_window.h
@@ -58,9 +58,9 @@ class BrowserView;
 }
 
 #if BUILDFLAG(IS_MAC)
-typedef gfx::NativeView NativeWindowHandle;
+using NativeWindowHandle = gfx::NativeView;
 #else
-typedef gfx::AcceleratedWidget NativeWindowHandle;
+using NativeWindowHandle = gfx::AcceleratedWidget;
 #endif
 
 class NativeWindow : public base::SupportsUserData,


Test patch: