From 45bb50bd917b4e082bf456d9bf9f1795e72736ca Mon Sep 17 00:00:00 2001 From: Wellington Pereira Date: Fri, 7 Aug 2026 13:37:05 -0300 Subject: [PATCH] [JEWEL-1388] Stop using `sun.misc.Unsafe` to resolve the native window handle closes https://github.com/JetBrains/intellij-community/pull/3604 (cherry picked from commit 5535ed3dcc6d84e420299a8f4b9a3b7481d34a10) IJ-MR-220560 GitOrigin-RevId: 0857256456bb959888af8ce85f45fd5645eb7f51 --- .../int-ui-standalone-tests/BUILD.bazel | 4 + .../int-ui-standalone-tests/build.gradle.kts | 8 +- ....platform.jewel.intUi.standalone.tests.iml | 2 + .../MacPlatformServicesReflectionTest.kt | 71 ---------- .../MacPlatformServicesWindowHandleTest.kt | 55 ++++++++ .../standalone/window/UnsafeAccessing.kt | 69 --------- .../window/macos/MacPlatformServices.kt | 131 +++++------------- 7 files changed, 100 insertions(+), 240 deletions(-) delete mode 100644 platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt create mode 100644 platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt delete mode 100644 platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel index a8ff0445dd99..0e8a5f2ca478 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel +++ b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel @@ -66,6 +66,10 @@ jvm_library( "//libraries/kotlinx/coroutines/test:test_test_lib", "//platform/jewel/ui", "//platform/jewel/ui:ui_test_lib", + "//libraries/compose-foundation-desktop", + "//libraries/compose-foundation-desktop:compose-foundation-desktop_test_lib", + "//libraries/jna", + "//libraries/jna:jna_test_lib", ], ) ### auto-generated section `build intellij.platform.jewel.intUi.standalone.tests` end diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts index d570d0f35c45..73e4962d9fbd 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts +++ b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts @@ -1,4 +1,8 @@ -plugins { jewel } +plugins { + jewel + alias(libs.plugins.composeDesktop) + alias(libs.plugins.compose.compiler) +} dependencies { api(projects.intUi.intUiStandalone) @@ -9,6 +13,8 @@ dependencies { testRuntimeOnly(libs.junit.platform.launcher) testImplementation(libs.kotlinx.coroutines.core) testImplementation(libs.kotlinx.coroutines.test) + testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } + testImplementation(libs.jna.core) } tasks.test { useJUnitPlatform() } diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml index 36f48c07db6b..65f76bf7cf3a 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml +++ b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml @@ -39,5 +39,7 @@ + + \ No newline at end of file diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt deleted file mode 100644 index 0b8c904ea6b4..000000000000 --- a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package org.jetbrains.jewel.intui.standalone.window.macos - -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import org.junit.jupiter.api.Test - -/** - * Regression test for JEWEL-1387: the reflective lookups used to resolve the native window must search the whole class - * hierarchy, not just the runtime class. - * - * On JBR 25 the AWT window peer is `sun.lwawt.macosx.LWCWindowPeer`, a subclass of `sun.lwawt.LWWindowPeer` that - * inherits (but does not declare) `getPlatformWindow()`. A `getDeclaredMethod` call on the runtime class alone throws - * [NoSuchMethodException] and silently breaks native window chrome updates on macOS. - */ -internal class MacPlatformServicesReflectionTest { - @Suppress("UnusedPrivateMember", "FunctionOnlyReturningConstant") - private open class BasePeer { - @JvmField internal val ptr: Long = 42L - - fun getPlatformWindow(): Any = this - } - - // Mimics JBR 25's LWCWindowPeer: inherits getPlatformWindow() and ptr without declaring them - private class SubclassPeer : BasePeer() - - @Test - fun `findMethodInHierarchy finds method declared on the class itself`() { - val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(BasePeer::class.java, "getPlatformWindow") - - assertNotNull(method, "Should find a method declared directly on the class") - } - - @Test - fun `findMethodInHierarchy finds method inherited from a superclass`() { - val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "getPlatformWindow") - - assertNotNull(method, "Should find a method declared on a superclass (JBR 25 LWCWindowPeer scenario)") - assertEquals(BasePeer::class.java, method.declaringClass, "Method should be resolved from the superclass") - } - - @Test - fun `findMethodInHierarchy returns null for a missing method`() { - val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "doesNotExist") - - assertNull(method, "Should return null instead of throwing for missing methods") - } - - @Test - fun `findFieldInHierarchy finds field declared on the class itself`() { - val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(BasePeer::class.java, "ptr") - - assertNotNull(field, "Should find a field declared directly on the class") - } - - @Test - fun `findFieldInHierarchy finds field inherited from a superclass`() { - val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "ptr") - - assertNotNull(field, "Should find a field declared on a superclass") - assertEquals(BasePeer::class.java, field.declaringClass, "Field should be resolved from the superclass") - } - - @Test - fun `findFieldInHierarchy returns null for a missing field`() { - val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "doesNotExist") - - assertNull(field, "Should return null instead of throwing for missing fields") - } -} diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt new file mode 100644 index 000000000000..e70eb5dd1801 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt @@ -0,0 +1,55 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.intui.standalone.window.macos + +import androidx.compose.ui.awt.ComposeWindow +import javax.swing.JFrame +import javax.swing.SwingUtilities +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledIf + +/** + * Regression test for JEWEL-1388: resolving the native `NSWindow*` must not require `sun.misc.Unsafe` or reflection + * into JDK-internal `sun.awt`/`sun.lwawt.macosx` classes. + */ +internal class MacPlatformServicesWindowHandleTest { + @Test + fun `getWindowFromJavaWindow returns NIL for a null window`() { + assertEquals(ID.NIL, MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(null)) + } + + @Test + @DisabledIf(HEADLESS) + fun `getWindowFromJavaWindow returns NIL for a non-Compose AWT window`() { + val frame = JFrame() + try { + assertEquals(ID.NIL, MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(frame)) + } finally { + frame.dispose() + } + } + + @Test + @DisabledIf(HEADLESS) + fun `getWindowFromJavaWindow returns the native handle of a realized ComposeWindow`() { + lateinit var window: ComposeWindow + try { + var handle = 0L + SwingUtilities.invokeAndWait { + window = ComposeWindow() + window.isVisible = true + handle = window.windowHandle + } + + assertNotEquals(0L, handle, "A displayed ComposeWindow must expose a native window handle") + assertEquals(ID(handle), MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(window)) + } finally { + SwingUtilities.invokeAndWait { window.dispose() } + } + } + + private companion object { + const val HEADLESS = "java.awt.GraphicsEnvironment#isHeadless" + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt deleted file mode 100644 index c5cd754ae306..000000000000 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package org.jetbrains.jewel.intui.standalone.window - -import java.lang.reflect.AccessibleObject -import java.util.logging.Level -import java.util.logging.Logger -import sun.misc.Unsafe - -internal object UnsafeAccessing { - private val logger = Logger.getLogger(UnsafeAccessing::class.java.simpleName) - - private val unsafe: Any? by lazy { - try { - val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") - theUnsafe.isAccessible = true - theUnsafe.get(null) as Unsafe - } catch (@Suppress("TooGenericExceptionCaught") error: Throwable) { - logger.log(Level.WARNING, "Unsafe accessing initializing failed.", error) - null - } - } - - val desktopModule by lazy { ModuleLayer.boot().findModule("java.desktop").get() } - - val ownerModule: Module by lazy { this.javaClass.module } - - private val isAccessibleFieldOffset: Long? by lazy { - try { - @Suppress("DEPRECATION") - (unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first")) - } catch (_: Throwable) { - null - } - } - - private val implAddOpens by lazy { - try { - Module::class.java.getDeclaredMethod("implAddOpens", String::class.java, Module::class.java).accessible() - } catch (_: Throwable) { - null - } - } - - fun assignAccessibility(obj: AccessibleObject) { - try { - val theUnsafe = unsafe as? Unsafe ?: return - val offset = isAccessibleFieldOffset ?: return - theUnsafe.putBooleanVolatile(obj, offset, true) - } catch (_: Throwable) { - // ignore - } - } - - fun assignAccessibility(module: Module, packages: List) { - try { - packages.forEach { implAddOpens?.invoke(module, it, ownerModule) } - } catch (_: Throwable) { - // ignore - } - } - - private class Parent { - var first = false - - @Volatile var second: Any? = null - } -} - -internal fun T.accessible(): T = apply { UnsafeAccessing.assignAccessibility(this) } diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt index 86fca4ad7b59..f98427a71a42 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt @@ -3,13 +3,10 @@ package org.jetbrains.jewel.intui.standalone.window.macos import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.awt.ComposeWindow import com.sun.jna.Callback import com.sun.jna.Pointer -import java.awt.Component import java.awt.Window -import java.lang.reflect.Field -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method import javax.swing.SwingUtilities import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.VisibleForTesting @@ -17,132 +14,63 @@ import org.jetbrains.jewel.foundation.InternalJewelApi import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.intui.standalone.styling.default import org.jetbrains.jewel.intui.standalone.styling.macOs -import org.jetbrains.jewel.intui.standalone.window.UnsafeAccessing -import org.jetbrains.jewel.intui.standalone.window.accessible import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior import org.jetbrains.skiko.hostOs +/** + * Provides macOS-specific platform services for window decoration and system preference observation. + * + * Implementations communicate with macOS APIs (via JNA/Objective-C) to apply native window chrome updates and read + * scrollbar settings from native sources such as NSUserDefaults (track-click behavior) and NSScroller (scroller style) + */ @ApiStatus.Internal @InternalJewelApi public interface MacPlatformServices { + /** Updates the native window's color scheme to match the current Jewel theme. */ public fun updateColors(w: Window) + /** Refreshes the full-screen button state in the native window title bar. */ public fun updateFullScreenButtons(w: Window) + /** Hides the system cursor until the next mouse-move event (delegates to `NSCursor`). */ public fun hideCursorUntilMoved() + /** Reads the current scrollbar track-click behavior from `NSUserDefaults`. */ public fun readScrollbarTrackClickBehavior(): TrackClickBehavior + /** Reads the current scrollbar visibility style from the native `NSScroller` preference. */ public fun readScrollbarVisibility(): ScrollbarVisibility + /** + * Registers [action] to be invoked whenever a scrollbar-related system preference changes. + * + * Observes both `NSPreferredScrollerStyleDidChangeNotification` (visibility) and + * `AppleNoRedisplayAppearancePreferenceChanged` (track-click behavior). + */ public fun onPreferencesChanged(action: () -> Unit) } +/** Default [MacPlatformServices] implementation that uses JNA to invoke native macOS (Objective-C) APIs. */ @ApiStatus.Internal @InternalJewelApi public object MacPlatformServicesDefaultImpl : MacPlatformServices { private val logger = JewelLogger.getInstance(MacPlatformServicesDefaultImpl::class.java.simpleName) private var nativeCallbackReference: Callback? = null // Keep a strong reference here to prevent GC - init { - try { - UnsafeAccessing.assignAccessibility( - UnsafeAccessing.desktopModule, - listOf("sun.awt", "sun.lwawt", "sun.lwawt.macosx"), - ) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - logger.warn("Assign access for jdk.desktop failed.", e) - } - } - - internal fun getWindowFromJavaWindow(w: Window?): ID { - if (w == null) { - return ID.NIL - } - try { - val cPlatformWindow = getPlatformWindow(w) - if (cPlatformWindow != null) { - val ptr = findFieldInHierarchy(cPlatformWindow.javaClass, "ptr") - if (ptr == null) { - logger.warn("Fail to get cPlatformWindow from awt window: no 'ptr' field found.") - return ID.NIL - } - ptr.setAccessible(true) - return ID(ptr.getLong(cPlatformWindow)) - } - } catch (e: IllegalAccessException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) - } - return ID.NIL - } - - public fun getPlatformWindow(w: Window): Any? { - try { - val awtAccessor = Class.forName("sun.awt.AWTAccessor") - val componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null) - val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible() - val peer = getPeer.invoke(componentAccessor, w) - if (peer != null) { - // The method may be declared on a superclass of the runtime peer class (e.g., on JBR 25 the peer - // is sun.lwawt.macosx.LWCWindowPeer, which inherits getPlatformWindow() from sun.lwawt.LWWindowPeer), - // so we must search the whole class hierarchy rather than just the runtime class. - val getPlatformWindowMethod = findMethodInHierarchy(peer.javaClass, "getPlatformWindow") - if (getPlatformWindowMethod == null) { - logger.warn("Fail to get cPlatformWindow from awt window: no getPlatformWindow() method found.") - return null - } - val cPlatformWindow = getPlatformWindowMethod.invoke(peer) - if (cPlatformWindow != null) { - return cPlatformWindow - } - } - } catch (e: IllegalAccessException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) - } catch (e: InvocationTargetException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) - } catch (e: ClassNotFoundException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) - } - return null - } - /** - * Finds a method with the given [name] and no parameters declared on [clazz] or any of its superclasses, or `null` - * if no such method exists. + * Returns the native `NSWindow*` backing the given AWT [Window], or [ID.NIL] if [w] isn't a [ComposeWindow] or the + * native surface hasn't been realized yet. + * + * Uses [ComposeWindow.windowHandle], Compose's own supported accessor, instead of reflecting into JDK-internal + * `sun.awt`/`sun.lwawt.macosx` classes (see JEWEL-1388). */ @VisibleForTesting @ApiStatus.Internal @InternalJewelApi - public fun findMethodInHierarchy(clazz: Class<*>, name: String): Method? { - var current: Class<*>? = clazz - while (current != null) { - try { - return current.getDeclaredMethod(name) - } catch (_: NoSuchMethodException) { - current = current.superclass - } - } - return null - } - - /** - * Finds a field with the given [name] declared on [clazz] or any of its superclasses, or `null` if no such field - * exists. - */ - @VisibleForTesting - @ApiStatus.Internal - @InternalJewelApi - public fun findFieldInHierarchy(clazz: Class<*>, name: String): Field? { - var current: Class<*>? = clazz - while (current != null) { - try { - return current.getDeclaredField(name) - } catch (_: NoSuchFieldException) { - current = current.superclass - } - } - return null + public fun getWindowFromJavaWindow(w: Window?): ID { + val handle = (w as? ComposeWindow)?.windowHandle ?: 0L + return if (handle == 0L) ID.NIL else ID(handle) } public override fun updateColors(w: Window) { @@ -282,6 +210,10 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices { } } + /** + * Executes [producer] inside a `NSAutoreleasePool` and returns its result, or `null` if the current OS is not macOS + * or if an exception is thrown (the exception is logged as a warning). + */ @Suppress("TooGenericExceptionCaught") public fun callMac(producer: () -> T?): T? { if (!hostOs.isMacOS) return null @@ -298,6 +230,7 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices { } } +/** [ProvidableCompositionLocal] that provides the active [MacPlatformServices] instance for the current window. */ @get:ApiStatus.Internal @InternalJewelApi public val LocalMacPlatformServices: ProvidableCompositionLocal = staticCompositionLocalOf {