Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions src/main/java/org/apache/groovy/util/HiddenClassDefiner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
/*
* 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.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.lang.reflect.Constructor;

/**
* Central facility for defining <em>hidden classes</em>
* (<a href="https://openjdk.org/jeps/371">JEP 371</a>).
*
* <h2>Why a single entry point</h2>
* <p>Every dynamic class generator in Groovy (proxies, reflectors, per-class
* meta-method artifacts, …) should obtain hidden classes through this type so
* that:
* <ul>
* <li>the {@code NESTMATE} / {@code STRONG} policy lives in one place;</li>
* <li>the package of the supplied class-file is automatically aligned with
* the lookup class (a hard requirement of
* {@link Lookup#defineHiddenClass});</li>
* <li>callers can use a soft {@code try*} API that never throws on the
* expected failure modes (module access denied, package mismatch after
* rewrite, linkage errors because the host class loader cannot see a
* referenced type) and simply returns {@code null} for transparent
* fall-back to {@link ClassLoader#defineClass}.</li>
* </ul>
*
* <h2>Preferred usage (host-based)</h2>
* <pre>{@code
* // host determines: defining loader, run-time package, protection domain, nest
* Class<?> hidden = HiddenClassDefiner.tryDefineNestmate(hostClass, bytecode, false);
* if (hidden == null) {
* // fall back to ClassLoader.defineClass(...)
* }
* }</pre>
*
* <p>The host is obtained via {@link MethodHandles#privateLookupIn(Class, Lookup)}
* using a full-privilege lookup captured inside this (Java) class. That makes
* the result independent of Groovy's indy / {@code $$InjectedInvoker}
* caller-sensitive quirks.
*
* <h2>Lifecycle</h2>
* <ul>
* <li><em>weak</em> (default for nestmates) — the JVM may unload the class as
* soon as its {@link Class} object becomes unreachable;</li>
* <li><em>strong</em> — lifetime is tied to the defining class loader.</li>
* </ul>
*
* <h2>Kill switch</h2>
* <p>{@code -Dgroovy.hidden.classes.disable=true} forces every {@code try*}
* method to return {@code null} (and makes {@link #isEnabled()} false) so
* diagnostics and legacy environments can fall back without code changes.
*
* @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);

/**
* Full-privilege lookup for <em>this</em> Java class, captured during
* {@code <clinit>}. Used solely as the caller argument to
* {@link MethodHandles#privateLookupIn(Class, Lookup)}; it is never used
* as the nest host of user-generated classes.
*/
private static final Lookup TRUSTED_LOOKUP = MethodHandles.lookup();

// Pre-allocated option arrays — defineHiddenClass is on the meta-class hot path.
private static final Lookup.ClassOption[] OPT_NESTMATE =
new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE};
private static final Lookup.ClassOption[] OPT_STRONG =
new Lookup.ClassOption[]{Lookup.ClassOption.STRONG};
private static final Lookup.ClassOption[] OPT_NESTMATE_STRONG =
new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE, Lookup.ClassOption.STRONG};
private static final Lookup.ClassOption[] OPT_NONE = new Lookup.ClassOption[0];

private HiddenClassDefiner() {
throw new AssertionError("HiddenClassDefiner is a utility class");
}

// -------------------------------------------------------------------------
// Status
// -------------------------------------------------------------------------

/**
* @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;
}

// -------------------------------------------------------------------------
// Soft (best-effort) API — preferred by production call sites
// -------------------------------------------------------------------------

/**
* Attempts to define {@code bytes} as a hidden <em>nestmate</em> of
* {@code host} with a weak (eager-unloading) lifecycle.
*
* <p>On success the returned class:
* <ul>
* <li>is non-discoverable by name ({@link Class#isHidden()} is true);</li>
* <li>shares the defining loader / package / protection domain of
* {@code host};</li>
* <li>is a nestmate of {@code host} (mutual private access).</li>
* </ul>
*
* <p>Returns {@code null} when hidden classes are disabled, {@code host}
* is unsuitable (null / primitive / array / hidden), private lookup is
* refused, the class file is invalid, or linkage fails (for example the
* host loader cannot resolve a supertype referenced by the bytecode).
* Callers are expected to fall back to {@link ClassLoader#defineClass}.
*
* @param host nest host and class-loader / package donor; must be a
* normal (non-hidden) reference type
* @param bytes class-file bytes; {@code this_class} is rewritten into
* {@code host}'s package when needed
* @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 Class<?> host,
final byte[] bytes,
final boolean initialize) {
if (HIDDEN_CLASSES_DISABLED || !isUsableHost(host) || bytes == null) {
return null;
}
try {
final Lookup hostLookup = MethodHandles.privateLookupIn(host, TRUSTED_LOOKUP);
final byte[] aligned = alignPackage(bytes, host);
return hostLookup.defineHiddenClass(aligned, initialize, OPT_NESTMATE).lookupClass();
} catch (IllegalAccessException | IllegalArgumentException | SecurityException | LinkageError e) {
return null;
} catch (RuntimeException e) {

Check warning on line 166 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Combine this catch with the one at line 164, which has the same body.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ_De73sETN2Y3sJgzAJ&open=AZ_De73sETN2Y3sJgzAJ&pullRequest=2755
// ASM rewrite failures, unexpected JVM checks, etc.
return null;
}
}

/**
* Soft variant of {@link #define(Lookup, byte[], boolean, boolean, boolean)}.
* Returns {@code null} instead of throwing for the expected failure modes.
*
* <p>The bytecode package is aligned to {@code lookup.lookupClass()} before
* definition. The lookup itself is not replaced — callers that need a
* specific nest host should obtain it via
* {@link MethodHandles#privateLookupIn(Class, Lookup)} (or use
* {@link #tryDefineNestmate(Class, byte[], boolean)}).
*/
public static Class<?> tryDefine(
final Lookup lookup,
final byte[] bytes,
final boolean initialize,
final boolean nestmate,
final boolean strong) {
if (HIDDEN_CLASSES_DISABLED || lookup == null || bytes == null) {
return null;
}
try {
return define(lookup, bytes, initialize, nestmate, strong);
} catch (IllegalAccessException | IllegalArgumentException | SecurityException | LinkageError e) {
return null;
} catch (RuntimeException e) {

Check warning on line 195 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Combine this catch with the one at line 193, which has the same body.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ_De73sETN2Y3sJgzAK&open=AZ_De73sETN2Y3sJgzAK&pullRequest=2755
return null;
}
}

// -------------------------------------------------------------------------
// Strict API — for tests and callers that want the original exception
// -------------------------------------------------------------------------

/**
* Defines a hidden class with the given options.
*
* <p>The class-file's {@code this_class} package is rewritten to match
* {@code lookup.lookupClass()} when they differ — this is required by
* {@link Lookup#defineHiddenClass}.
*
* @param lookup full-privilege lookup whose lookup-class supplies the
* defining loader, package, protection domain and
* (when {@code nestmate}) nest
* @param bytes class-file bytes
* @param initialize whether to initialize the class immediately
* @param nestmate whether to inject the class into the lookup class's nest
* @param strong whether the class's lifetime is tied to the loader
* @return the defined hidden class
* @throws IllegalAccessException if the lookup lacks the required access
* @throws IllegalArgumentException if the bytes are not a valid class file
* (after package alignment)
* @throws LinkageError if a dependency of the new class cannot
* be resolved in the lookup class's loader
*/
public static Class<?> define(
final Lookup lookup,
final byte[] bytes,
final boolean initialize,
final boolean nestmate,
final boolean strong) throws IllegalAccessException {
final byte[] aligned = alignPackage(bytes, lookup.lookupClass());
return lookup.defineHiddenClass(aligned, initialize, options(nestmate, strong)).lookupClass();
}

/**
* Strict nestmate + weak convenience overload.
*
* @see #define(Lookup, byte[], boolean, boolean, boolean)
*/
public static Class<?> defineNestmate(
final Lookup lookup,
final byte[] bytes,
final boolean initialize) throws IllegalAccessException {
return define(lookup, bytes, initialize, true, false);
}

/**
* Strict non-nestmate + strong convenience overload.
*
* @see #define(Lookup, byte[], boolean, boolean, boolean)
*/
public static Class<?> defineStrong(
final Lookup lookup,
final byte[] bytes,
final boolean initialize) throws IllegalAccessException {
return define(lookup, bytes, initialize, false, true);
}

// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------

/**
* Returns a declared constructor of {@code type}, wrapping a missing
* constructor as {@link IllegalStateException} (bytecode-generation bug).
*/
@SuppressWarnings("unchecked")
public static <T> Constructor<T> findConstructor(
final Class<T> type,
final Class<?>... parameterTypes) {
try {
return type.getDeclaredConstructor(parameterTypes);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Class " + type.getName() + " is missing the expected constructor", e);
}
}

/**
* Rewrites {@code this_class} (and all internal references to it) so the
* class lives in {@code host}'s run-time package. No-op when already aligned.
*
* <p>Visible for testing.
*/
static byte[] alignPackage(final byte[] bytes, final Class<?> host) {
final String hostPkg = host.getPackageName();
final ClassReader reader = new ClassReader(bytes);
final String oldInternal = reader.getClassName();
final int slash = oldInternal.lastIndexOf('/');
final String simple = slash < 0 ? oldInternal : oldInternal.substring(slash + 1);
final String newInternal = hostPkg.isEmpty()
? simple
: hostPkg.replace('.', '/') + '/' + simple;
if (oldInternal.equals(newInternal)) {
return bytes;
}
final ClassWriter writer = new ClassWriter(reader, 0);
reader.accept(new ClassRemapper(writer, new SimpleRemapper(oldInternal, newInternal)), 0);

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (grails-ad, \\.perf\\.grails\\.[A-D])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (bench, \\.bench\\.)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (core-ag, \\.perf\\.[A-G])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (grails-ez, \\.perf\\.grails\\.[E-Z])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (core-hz, \\.perf\\.[H-Z])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / dist

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (bench, \\.bench\\.)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (core-ag, \\.perf\\.[A-G])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (grails-ad, \\.perf\\.grails\\.[A-D])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (core-hz, \\.perf\\.[H-Z])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test (grails-ez, \\.perf\\.grails\\.[E-Z])

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / dist

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / test

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / lts (25, ubuntu-latest)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / lts (17, macos-latest)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (19)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / lts (17, ubuntu-latest)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (20)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (23)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / lts (21, ubuntu-latest)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (22)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (24)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (18)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated

Check warning on line 298 in src/main/java/org/apache/groovy/util/HiddenClassDefiner.java

View workflow job for this annotation

GitHub Actions / additional (26)

[deprecation] SimpleRemapper(String,String) in SimpleRemapper has been deprecated
return writer.toByteArray();
}

private static boolean isUsableHost(final Class<?> host) {
return host != null
&& !host.isPrimitive()
&& !host.isArray()
&& !host.isHidden();
}

private static Lookup.ClassOption[] options(final boolean nestmate, final boolean strong) {
if (nestmate) {
return strong ? OPT_NESTMATE_STRONG : OPT_NESTMATE;
}
return strong ? OPT_STRONG : OPT_NONE;
}
}
Loading
Loading