[
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)