mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0b3921720a
commit
45bb50bd91
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
|
||||
+2
@@ -39,5 +39,7 @@
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.test" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.ui" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.compose.foundation.desktop" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.jna" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
-71
@@ -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")
|
||||
}
|
||||
}
|
||||
+55
@@ -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"
|
||||
}
|
||||
}
|
||||
-69
@@ -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<String>) {
|
||||
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 : AccessibleObject> T.accessible(): T = apply { UnsafeAccessing.assignAccessibility(this) }
|
||||
+32
-99
@@ -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 <T : Any> 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<MacPlatformServices> = staticCompositionLocalOf {
|
||||
|
||||
Reference in New Issue
Block a user