[ 
https://issues.apache.org/jira/browse/GROOVY-12223?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18101455#comment-18101455
 ] 

ASF GitHub Bot commented on GROOVY-12223:
-----------------------------------------

blackdrag commented on code in PR #2755:
URL: https://github.com/apache/groovy/pull/2755#discussion_r3706422829


##########
src/main/java/org/apache/groovy/util/HiddenClassDefiner.java:
##########
@@ -0,0 +1,215 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.util;
+
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.SimpleRemapper;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.util.Collections;
+
+/**
+ * Central facility for defining <em>hidden classes</em>
+ * (<a href="https://openjdk.org/jeps/371";>JEP 371</a>).
+ *
+ * <h2>Lookup ownership (read this first)</h2>
+ * <p>{@link MethodHandles#lookup()} is <em>caller-sensitive</em>: it returns a
+ * full-privilege lookup only for the class that literally contains the call.
+ * A lookup captured in this utility therefore only has full privilege for
+ * {@code HiddenClassDefiner} itself — never for arbitrary foreign classes
+ * (for example {@code java.lang.String} in {@code java.base}).
+ *
+ * <p>Consequently the <strong>preferred</strong> entry point is
+ * {@link #tryDefineNestmate(Lookup, byte[], boolean)}, where the caller
+ * supplies a {@link Lookup} obtained inside the intended nest-host class:
+ * <pre>{@code
+ * // Inside the class that should host the hidden nestmate:
+ * private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+ *
+ * Class<?> hidden = HiddenClassDefiner.tryDefineNestmate(LOOKUP, bytecode, 
false);
+ * if (hidden == null) {
+ *     // fall back to ClassLoader.defineClass(...)
+ * }
+ * }</pre>
+ *
+ * <p>The overload {@link #tryDefineNestmate(Class, byte[], boolean)} is a
+ * <em>best-effort</em> helper for foreign hosts (user classes Groovy does not
+ * control). It uses {@link MethodHandles#privateLookupIn(Class, Lookup)} from
+ * this class and therefore succeeds only when the host's package is accessible
+ * to Groovy's module — typically true for unnamed-module application classes,
+ * and typically false for sealed / unopened packages such as those in
+ * {@code java.base}. Callers must always handle a {@code null} result.
+ *
+ * <h2>What this utility centralises</h2>
+ * <ul>
+ *   <li>{@code NESTMATE} + weak-lifecycle policy for dynamic Groovy 
classes;</li>
+ *   <li>rewriting {@code this_class} into the lookup class's package (required
+ *       by {@link Lookup#defineHiddenClass});</li>
+ *   <li>a soft API that returns {@code null} on the <em>expected</em> failure
+ *       modes ({@link IllegalAccessException}, {@link 
IllegalArgumentException},
+ *       {@link SecurityException}, {@link LinkageError}) so call sites fall 
back
+ *       to {@link ClassLoader#defineClass} with one null check. Unexpected
+ *       failures (e.g. programming errors in callers) are not swallowed as a
+ *       blanket {@link RuntimeException}.</li>
+ * </ul>
+ *
+ * <h2>Kill switch</h2>
+ * <p>{@code -Dgroovy.hidden.classes.disable=true} forces every {@code try*}
+ * method to return {@code null}.
+ *
+ * @since 6.0.0
+ * @see Lookup#defineHiddenClass(byte[], boolean, Lookup.ClassOption...)
+ */
+public final class HiddenClassDefiner {
+
+    /** System property that disables hidden-class definitions. */
+    public static final String PROPERTY_DISABLE = 
"groovy.hidden.classes.disable";
+
+    /**
+     * {@code true} when hidden-class definitions are globally disabled.
+     * Evaluated once at class-init so hot paths pay no property-lookup cost.
+     */
+    public static final boolean HIDDEN_CLASSES_DISABLED =
+            SystemUtil.getBooleanSafe(PROPERTY_DISABLE, false);
+
+    /**
+     * Lookup for <em>this</em> class only — used exclusively as the caller
+     * argument to {@link MethodHandles#privateLookupIn(Class, Lookup)} in the
+     * foreign-host overload. It is never used as a nest host for user code.
+     */
+    private static final Lookup LOOKUP = MethodHandles.lookup();
+
+    /** Nestmate + weak (eager unloading) — the only option set production 
uses. */
+    private static final Lookup.ClassOption[] NESTMATE_WEAK =
+            new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE};
+
+    private HiddenClassDefiner() {
+    }
+
+    /**
+     * @return {@code true} when hidden-class definition is enabled
+     *         (the default unless {@value #PROPERTY_DISABLE} is set)
+     */
+    public static boolean isEnabled() {
+        return !HIDDEN_CLASSES_DISABLED;
+    }
+
+    /**
+     * Defines {@code bytes} as a hidden nestmate of {@code 
lookup.lookupClass()}
+     * with a weak lifecycle.
+     *
+     * <p>The lookup must have been obtained via {@link MethodHandles#lookup()}
+     * inside the intended nest-host class (or otherwise carry full privilege
+     * for that class). The class-file package is rewritten to match the lookup
+     * class before definition.
+     *
+     * @param lookup     full-privilege lookup for the nest host
+     * @param bytes      class-file bytes
+     * @param initialize {@code true} to run {@code <clinit>} immediately
+     * @return the hidden class, or {@code null} if definition is not possible
+     */
+    public static Class<?> tryDefineNestmate(
+            final Lookup lookup,
+            final byte[] bytes,
+            final boolean initialize) {
+        if (HIDDEN_CLASSES_DISABLED || lookup == null || bytes == null) {

Review Comment:
   Did I see it wrong or is this effectively called only with 
MethodHandles.lookup()? Does it really matter if it is one time 
HiddenClassDefiner and another time ReflectorLoader?





> Introduce hidden class support
> ------------------------------
>
>                 Key: GROOVY-12223
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12223
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> h2. Background
> Groovy generates many short-lived synthetic classes at runtime, including:
> * map/interface proxies ({{ProxyGeneratorAdapter}})
> * reflection dispatch helpers ({{Reflector}} / {{ReflectorLoader}})
> * per-class meta-method artifacts ({{ClassLoaderForClassArtifacts}})
> Today these are defined with {{ClassLoader#defineClass}} as ordinary *named* 
> classes. That has three practical downsides:
> # *Name pollution* — the synthetic types are discoverable via 
> {{Class.forName}} / {{ClassLoader#loadClass}}.
> # *Metaspace pressure* — their lifetime is tied to the defining class loader; 
> long-running applications that generate many artifacts retain them until the 
> loader itself is collected.
> # *Access friction* — without nest membership, generated code cannot share 
> private access with the host class the way a true nestmate can.
> JDK 15 introduced *hidden classes* ([JEP 371|https://openjdk.org/jeps/371]): 
> classes defined through {{Lookup#defineHiddenClass}} that are 
> non-discoverable by name, may join an access-control nest ({{NESTMATE}}), and 
> may be unloaded independently of the defining loader when not marked 
> {{STRONG}}.
> Groovy 6 requires JDK 17+, so the API is always present on supported runtimes.
> h2. Proposal
> Centralise hidden-class definition behind a single utility and prefer it for 
> the dynamic class-generation sites listed above, with a transparent fallback 
> to the existing {{ClassLoader#defineClass}} path.
> h3. New API
> {{org.apache.groovy.util.HiddenClassDefiner}} — the only call-site that 
> invokes {{Lookup#defineHiddenClass}}:
> * {{defineHiddenClass(lookup, bytes, initialize, nestmate, strong)}} — full 
> control
> * {{defineNestmateClass(lookup, bytes, initialize)}} — nestmate + weak 
> lifecycle (default for proxies / reflectors / artifacts)
> * {{defineStrongHiddenClass(lookup, bytes, initialize)}} — non-discoverable, 
> loader-tied lifetime
> * helpers: {{privateLookupIn(hostClass)}}, {{findConstructor(hiddenClass, 
> ...parameterTypes)}}
> Kill-switch (evaluated once at class-init for hot-path cost):
> {noformat}
> -Dgroovy.hidden.classes.disable=true
> {noformat}
> When disabled (or when private lookup / definition fails), callers fall back 
> to defining a normal visible class.
> h3. Integration points
> || Site || Nest host || Preferred options || Fallback ||
> | {{ClassLoaderForClassArtifacts#define}} | target (klazz) | nestmate, weak | 
> {{ClassLoader#defineClass}} + protection domain |
> | {{ProxyGeneratorAdapter}} | non-{{Object}} superclass if present; else 
> {{ProxyGeneratorAdapter}} | nestmate, weak | {{InnerLoader#defineClass}} |
> | {{ReflectorLoader#defineClass}} | {{Reflector}} | nestmate, weak | 
> {{ClassLoader#defineClass}} + protection domain |
> Behaviour for callers of these generators is unchanged: proxies still 
> implement the requested interfaces, reflectors still dispatch, artifacts 
> still construct. The only observable differences when the hidden path 
> succeeds are the synthetic name form (contains {{/}}) and {{Class#isHidden() 
> == true}}.
> h2. Benefits
> * Non-discoverable synthetic types (cleaner class-space / tooling view).
> * Nestmate private access where the nest host can be opened for private 
> lookup.
> * Eager unloading of weak hidden classes reduces long-run metaspace retention 
> for short-lived proxies and artifacts.
> * One policy / upgrade point if future JDKs add further 
> {{Lookup.ClassOption}} values.
> h2. Compatibility
> * Default-on when the JVM can obtain a full-privilege lookup for the chosen 
> nest host; silent fallback otherwise (e.g. sealed / unopened module packages).
> * Opt-out: {{-Dgroovy.hidden.classes.disable=true}}.
> * No public language-surface change; no change to successful proxy / 
> reflector / artifact *behaviour*, only to how the {{Class}} is defined.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to